C# 高级语法
前言
关于C#高级语法在重新拾起后发现很多有趣以及好用的特性与语法糖
本篇关于高级语法包括: 属性 常量 字段 泛型 委托 事件 匿名函数 lambda 协变逆变 预处理指令 反射 特性 多线程 迭代器
属性 get set
定义 :属性Property—-看起来像变量的函数 如下
1 |
|
这边定义了一个Person的类
1 | class Person { |
对于get set 语句块是可以在返回之前添加一点逻辑规则的(毕竟是
语句块可以类似函数来执行一些操作)
1 | public string Name{ |
对于get set 的语法块的特性可以将数据进行加密和解密处理
这边可以使用加密解密的算法
1 | public int Money{ |
自动属性,如果代码中没有特殊处理可以使用自动属性
1 | public int age { get; set;} |
索引器
可以像数组一样用索引访问其中元素,使程序看起来更直观,更容易编写,也写在get set 语句块中。
定义类
1 | class Person { |
其他概念与总结
- 成员属性概念:一般是用来保护成员变量的
- 成员属性的使用和变量一样 外部用对蒙点出
- get中需要return内容;set中用value表示传入的内容
- get和set语句块中可以加逻辑处理
- get和set可以加访问修饰符,但是要按照一定的规则进行添加
- get和set可以只有一个
- 自动属性是属性语句块中只有get和set,一般用于外部能得不能政这种情况
常量 Const static
常量的基本概念
常量的特点
常量的值必须在编译时确定,简单说就是在定义是设置值,以后都不会被改变了,她是编译常量。
常量只能用于简单的类型,因为常量值是要被编译然后保存到程序集的元数据中,只支持基元类型,如int、char、string、bool、double等。
常量在使用时,是把常量的值内联到IL代码中的,常量类似一个占位符,在编译时被替换掉了。正是这个特点导致常量的一个风险,就是
不支持跨程序集版本更新
;
partial让类的定义放在多个地方
字段
在C#中,字段(Field)是类、结构体或枚举的一个成员,它用于存储某种类型的数据。字段可以是任何有效的C#数据类型,包括基本数据类型(如int、float、bool等)、用户定义的类型(如类、结构体等)、枚举类型或数组等。字段是类的状态的一部分,它们用于存储与类的实例或类型本身相关的信息。
字段通常定义在类的主体内部,作为类的成员。它们可以是静态的(static),这意味着字段属于类本身而不是类的任何特定实例;或者它们可以是实例字段,这意味着每个类的实例都有其自己的字段副本。
下面是一个简单的C#类,其中包含两个字段的示例:
1 | public class Person |
在这个例子中,Person
类有两个实例字段:name
和 age
,它们分别存储人的名字和年龄。还有一个静态字段 totalPersons
,用于跟踪创建的 Person
对象总数。
字段通常是私有的(private),这意味着它们只能在类的内部被访问。如果需要从类的外部访问字段的值,通常会使用公共属性(public property)来提供对字段的访问,如上例中的 Name
和 Age
属性。
字段的初始化通常发生在构造函数中,或者通过字段声明时的初始值来完成。例如:
1 | public class Example |
在这个例子中,exampleField
字段在声明时被初始化为42。
字段是面向对象编程中封装概念的一部分,它们允许将数据与操作这些数据的方法一起封装在类中,从而隐藏实现细节并暴露有限的接口给类的用户。
泛型
1 | class TestClass<T,U> |
泛型约束
让泛型的类型有一定的限制
关键字:where
1 | class Test<T> where T: ‘不同约束的替换区域 ’{ |
泛型约束 | 语法 | 其他代码块 | Main.cs | 注释 |
---|---|---|---|---|
值类型 | class Test1 |
Test1 Test1 |
int float 是值类型sturct | |
引用类型 | class Test2 |
Test2 |
Random是引用类型class | |
公共无参构造约束:存在无参公共构造函数非抽象类型 | class Test3 |
class Test0{ public Test0(){}} class Test1{ public Test1(int a){ }} class Test2 { private Test2(){}} |
正确:Test3 错误: Test3 |
Test0-2只有Test0满足构造函数是公共且无参,这边Test0 也可以省略写构造函数 int作为值类型的构造函数都是公共无参所以可以使用 注意这边不能实例化虚类 |
类约束:某个类本身或者其派生类(子类) | class Test4 |
class child : parent{} | 正确:Test 4 错误:Test4 |
对于Test 1来说 Test2是它的子类可以使用,object是它的父类不能使用 |
某个接口的派生类型 | class Test5 |
interface IFly{} class Fly:IFly{} interface IFlyChild :IFly{} |
Test 5 对于value的实例化: 错误:t.value=new Test5 正确t.value=new Test5 |
在IFly接口中可以使用本身,子接口,子类,但是在实例化中,接口不能被实例化所以用里式替换,子类Fly代替了IFly; |
另一个泛型类型本身或者派生类型 | class Test6 <T,U> where T :U{ } | class child : parent{} | Test 6<parent,child>=new Test6<parent,child>(); Test6<parent,parent>=new Test6<parent,parent>(); |
在约束中只能使用本身或者子类 |
多个约束 | 代码 | |
---|---|---|
约束的组合使用 | class Test 7 where T:class,new(){} | 一起使用的排列组合有可能会报错 |
多个泛型有约束 | class Test8<T,K> where T: class , new() where K: new(){} |
多个约束:
可以通过在 where 子句中使用逗号来组合多个约束。例如,可以要求泛型类型参数既实现一个接口又继承一个基类。
1 |
|
委托Delegate
什么是委托?简单来说,委托类似于 C或 C++中的函数指针,允许将方法作为参数进行传递。
C#中的委托都继承自System.Delegate类型;
委托类型的声明与方法签名类似,有返回值和参数;
委托是一种可以封装命名(或匿名)方法的引用类型,把方法当做指针传递,但委托是面向对象、类型安全的
- delegate :可以保存多个函数指针 用+= -=进行操作
- Action :无返回值的delegate
- Func: 有返回值的delegate
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30//定义委托类CalcFunc
delegate float CalcFunc(float val1 ,float val2);
// 初始化listenerFunc为 Add 函数
CalcFunc listenerFunc = Add;
// 调用 listenerFunc 并打印结果
listenerFunc(3, 4);
// 3+4=7(Add函数)
// 将 listenerFunc 加一个 Sub 函数
listenerFunc += Sub;
// 调用 listenerFunc 并打印结果
listenerFunc(3, 4);
//3+4=7 (Add函数)
//3-4=-1(Sub函数被加入)
// 将 listenerFunc 减去一个 Sub 函数
listenerFunc -= Sub;
// 调用 listenerFunc 并打印结果
listenerFunc(3, 4);
//3+4=7 (此时只运行了Add函数)
// Add 函数
static int Add(int a, int b) =>Console.WriteLine($“{a}+{b}={a+b}”);
// Sub 函数
static int Sub(int a, int b) => Console.WriteLine($”{a}-{b}={a-b}”);
}
1 | Action<string>myPrint= (info)=>{ |
事件event
- 事件是基于委托的存在
- 事件的委托的安全包裹
- 让委托更具有安全性
- 事件是一种特殊的变量类型
申明语法:
访问修饰符 event 委托类型 事件名;
事件的使用:
- 事件是作为 成员变量存在于类中
- 委托怎么用 事件就怎么用
事件相对于委托的区别: - 不能在类外部 赋值
- 不能在类外部 调用
注意:
只能作为成员存在与类和接口以及结构体中
1 | class Test { |
1 | class Program{ |
为什么有事件
- 防止外部随意调用委托
- 防止外部随意置空委托
- 事件相当于对委托进行了一次封装,让其更安全
我自己认为事件是一种类似私有类的委托
匿名函数
顾名思义,就是没有名字的函数
匿名函数的使用主要是配合委托和事件进行使用
//脱离委托和事件 是不会使用匿名函数的
知识点二 基本语法
//delegate(参数列表)
//
//函数逻辑
//};
何时使用?
- 函数中传递委托参数时
- 委托或事件赋值时
这里声明了一个委托Action(无返回值的委托) 然后赋值(初始化)为匿名函数
1 | //Action.cs |
lambda表达式
使用上和匿名函数一样的
1 | Action a1 =()=>{ Console.WriteLine(“a1”);}; |
闭包:
内层函数可以引用包含在它外层的函数的变量
即使外层函数的执行已经终止
注意:
该变量提供的值并非变量创建时候的值,而是在父函数范围内的最终值
1 | // 定义一个整数变量 |
- 共享的局部变量i被提升为公共字段(static)
- 变量i的生命周期延长了;
- for循环结束后字段i的值是10了;
- 后面再次调用委托方法,肯定就是输出10了;
1 | Action action; |
那该如何修正呢?很简单,委托方法使用一个临时局部变量就OK了,不共享数据:
1 |
|
协变逆变
什么是协变逆变
里氏替换原则
其核心思想是“子类型必须能够替换其基类型”,也就是说,子类对象应该能够替换掉父类对象并且不影响程序的正确性。
子类必须完全实现父类的抽象方法。也就是说,子类不能删除父类中声明的方法,而且子类可以有自己的方法,但不能修改父类的方法。
子类可以有自己的行为,但不能影响父类原有的行为。也就是说,子类可以扩展父类的功能,但不能改变父类原有的功能。
子类的方法的前置条件(即方法的输入参数)不能比父类的方法的前置条件更严格。也就是说,子类的方法的参数范围必须比父类的方法更宽松,这样才能确保子类对象能够替换父类对象。
子类的方法的后置条件(即方法的返回值)不能比父类的方法的后置条件更宽松。也就是说,子类的方法的返回值类型必须比父类的方法更严格,这样才能确保程序的正确性。
协变 | 逆变 | |
---|---|---|
字面意思 | 和谐的变化,自然的变化 | 你常规的变化,不正常的变化 |
关键字 | 协变:out | 逆变:in |
解释 | 子类装父类 | 父类装子类 |
exp | 比如string 变成 object | 比如 object 变成 string |
作用
协变和逆变是用来修饰泛型的
用于在泛型中 修饰 泛型字母的
只有泛型接口和泛型委托能使用
在C#中,协变(Covariance)和逆变(Contravariance)是泛型编程的两个重要概念,它们主要涉及到泛型接口、委托和方法中的类型参数的使用方式。
这些概念有助于我们更灵活地处理不同的类型,特别是当涉及到继承关系时。
协变(Covariance)
协变允许我们将派生类型的对象赋值给基类型的引用。在C#中,协变主要应用于泛型接口和委托的返回类型。
协变接口
在C# 4.0及更高版本中,我们可以使用out
关键字来标记泛型接口中的协变类型参数。这意味着接口方法的返回类型可以是该类型参数的派生类型。
例如:
1 | interface ICovariant<out T> |
在上面的例子中,ICovariant<out T>
是一个协变接口,它的Get
方法返回一个T
类型的对象。由于Derived
是Base
的派生类,因此DerivedProvider
类可以隐式地实现ICovariant<Derived>
接口,即使它的基类BaseProvider
只实现了ICovariant<Base>
。
协变委托
类似的,协变也适用于委托。在委托的返回类型上使用out
关键字,可以允许委托指向返回派生类型的方法。
逆变(Contravariance)
逆变允许我们将基类型的对象赋值给派生类型的引用。在C#中,逆变主要应用于泛型接口和委托的参数类型。
逆变接口
使用in
关键字可以标记泛型接口中的逆变类型参数。这意味着接口方法的参数类型可以是该类型参数的基类型。
例如:
1 | interface IContravariant<in T> |
在这个例子中,IContravariant<in T>
是一个逆变接口,它的Set
方法接受一个T
类型的参数。由于Derived
是Base
的派生类,因此我们可以将Derived
类型的对象传递给只期望Base
类型参数的Set
方法。
逆变委托
类似地,逆变也适用于委托。在委托的参数类型上使用in
关键字,可以允许委托指向接受基类型参数的方法。
协变和逆变为C#的泛型编程提供了极大的灵活性,使得我们可以在不改变现有代码的情况下,更加轻松地处理类型之间的关系。
1 | class Father{ } |
预处理指令
C# 中并没有像 C 或 C++ 中的预处理指令(如 #include
, #define
, #ifdef
等)那样的功能。C# 是一种更高级的语言,其编译器不处理预处理步骤,而是在编译过程中直接处理源代码。
但是,C# 提供了一些与预处理指令类似的功能或概念,尽管它们的工作方式与传统的预处理指令不同。以下是一些与预处理相关的 C# 功能和概念:
- #define 和 #undef: C# 提供了
#define
和#undef
预处理指令,但它们主要用于条件编译。这些指令通常在项目属性或编译器的命令行参数中设置,而不是在源代码文件中。
1 |
|
然而,要注意的是,与 C 和 C++ 中的 #define
不同,C# 中的 #define
并不创建宏或替换文本。它仅仅是一个编译时的标志。
2. 条件编译: 通过 #if
, #else
, #elif
, #endif
指令,你可以在编译时基于某些条件包含或排除代码块。这通常用于调试、平台特定的代码或配置特定的功能。
3. 元数据: C# 使用元数据来描述类型、方法和属性等。这与 C 或 C++ 中的预处理指令不同,但提供了类似的功能,允许在编译时检查类型信息。
4. 编译常量和条件属性: 你可以在项目属性中设置编译常量,或在编译器的命令行参数中设置它们。此外,你还可以使用条件属性(如 [Conditional("DEBUG")]
)来标记方法,以便它们仅在特定的编译条件下被调用。
5. 区域指令: #region
和 #endregion
指令允许你在 Visual Studio 等 IDE 中折叠和展开代码块,这对于组织大型代码文件非常有用。
总的来说,尽管 C# 没有与 C 或 C++ 完全相同的预处理指令,但它提供了一套功能强大的工具和技术,允许你在编译时根据条件包含或排除代码。
反射
什么是程序集
程序集是经由编译器编译得到的,更进一步编译执行的那个中间产物
在WINDOWS系统中,它一般表现为后缀为.dll(库文件)或者是/exe(可执行文件) 的格式
说人话:
程序集就是我们与的一个代码集合,我们现在写的所有代码
最终都会被编译器翻译为一个程序集供别人使用
比如一个代码库文件 (dll)或者一个可执行文件(exe)
元数据
元数据就是用来描述数据的数据
这个概念上不仅仅用于程序上那个在别的领域也有元数据
说人话:
程序中的类,类中的函数,变量等等信息就是 程序的 元数据
有关程序以及类型的数据被称为 元数据,它们保存在程序集中
反射的概念
程序正在运行时,可以查看其它程序集或者自身的元数据。
一个运行的程序查看本身或者其它程序的元数据的行为就叫做反射
在程序运行时,通过反射可以得到其它程序集或者自己程序集代码的各种信息
类,函数,变量,对象等等,实例化它们,执行它们,操作它们
反射的作用
因为反射可以在程序编译后获得信息,所以它提高了程序的拓展性和灵活性
- 程序运行时得得到所有元数据,包括元数据的特性
- 程序运行时,实例化对象,操作对象
- 程序运行时创建新对象,用这些对象执行任务
方法 | 描述 | 示例代码 |
---|---|---|
使用对象的GetType() 方法 |
在任何对象中,可以通过调用GetType() 方法来获取该对象的类型。 |
csharp int a = 42; Type type = a.GetType(); Console.WriteLine(type); |
使用typeof 关键字 |
通过typeof 关键字并传入类名,可以直接获取到对应类型的Type。 |
csharp Type type2 = typeof(int); Console.WriteLine(type2); |
通过类的全名获取类型 | 通过Type.GetType() 方法,传入包含命名空间的完整类名,可以获取到对应的Type。 |
csharp Type type3 = Type.GetType("System.Int32"); Console.WriteLine(type3); |
在这上面三个例子实际栈中实际指向的堆地址都是一样的
得到类的程序集信息(了解)
Console.WriteLine(type.Assembly);
获取类中所有的公共成员
Type t=typeof(Test);//本文件常用,如果是不同文件的话就用类名找
MemberInfo[]infos=t.GetMembers();
- 获取类的公共构造函数并调用
你的文字描述中存在一些语法错误和拼写错误,我会尝试将其整理并修改成合理的C#代码。根据你的描述,你想要执行一个无参构造方法和一个有参构造方法,并且打印出构造出来的对象的某个属性。
首先,我们整理并修改无参构造的部分:
1 | // 2-1 得到无参构造 |
然后,我们整理并修改有参构造的部分。这里假设Test
类有一个接受int
和string
类型参数的构造方法,并且你想要构造一个对象并打印它的str
属性(假设Test
类有这个属性):
1 | // 2-2 得到有参构造(一个int参数) |
请注意,t
应该是一个Type
对象,代表你想要反射的类。同时,Test
类应该已经定义,并且包含你尝试访问的i
和str
属性或字段。此外,请确保反射的类已经被加载到当前的执行上下文中,否则GetConstructor
方法可能会返回null
。
获取指定名称的公共成员变量
获取类的公共成员方法
https://www.zhihu.com/tardis/zm/art/493738261?source_id=1005
Activator
用于快速实例化对象的类
用于将Type对象快捷实例化为对象
先得到Type
然后 快速实例化一个对象
Type testType=typeof(Test);
//无参构造
Test testObj=Activator.CreateInstance(testType)
特性
在C#中,特性(Attributes)是用于为程序中的类型和成员提供元数据(metadata)的一种机制。特性允许开发人员在源代码中附加声明性信息,以供编译器、运行时和其他工具使用。
以下是关于C#特性的一些重要概念和用法:
声明特性:特性本身是一种自定义的类,通过在类、方法、属性等声明前面使用方括号并提供特性类的名称来使用特性。例如:
1
2[ ]
public class MyClass { }内置特性:C#提供了一些内置的特性,例如
[Serializable]
、[Obsolete]
等。这些特性可以用来标记类型和成员的属性,告诉编译器或其他工具如何处理它们。自定义特性:开发人员可以根据需要创建自定义特性。自定义特性类通常派生自
System.Attribute
类,可以包含字段、属性和方法等。例如:1
2
3
4
5[ ]
public class CustomAttribute : Attribute
{
// 构造函数、属性和方法等
}应用特性:可以将特性应用于程序中的各种元素,如类、方法、属性、参数等。特性可以使用单个实例应用于多个元素,也可以使用多个实例应用于单个元素。
获取特性信息:可以使用反射机制在运行时获取特性信息。通过检查类型和成员上的特性,可以动态地获取元数据并做出相应的决策。
特性参数:特性可以具有参数,这些参数可以在特性的使用中提供。例如:
1
2[ ]
public class MyClass { }使用条件:特性可以具有使用条件,指定了特性可以应用于的元素类型、范围和其他限制条件。
通过使用特性,开发人员可以为代码添加元数据,以提供更多的信息给编译器、运行时和其他工具,从而实现更灵活、更健壮的程序设计和开发。
语法糖
- var隐式类型:
var i=5;
var不能作为类的成员只能用于临时变量声明。只能存在于函数语句块中且必须初始化
- 设置对象初始值
Person p=new Person{sex=true,Age=18,name=“Frag”};
Person p=new Person(){sex=true,Age=18,name=“Frag”};
初始化可以不用写全,且是不用调用构造函数的初始化
小括号可以加只不过是调用了构造函数
- 设置集合初始值
ListlistPerson=new List {
new Person(200),
new Person(100){Age=18},
new Person{sex=true,Age=18,name=“Frag”}
};
Dictionary<int,string>=new Dictionary<int,string>{
{1,”2333”},
{2,”6666”}
};
- 匿名类型
var变量可以声明为自定义的匿名类型
var v=new {age=10,money=11,name=“Frag”};
Console.WriteLine(v.age);
- 可空类型
值类型不能为空
int c=null;(error)(可以赋值为空)
int? c=null;判断是否为空
if(c.HasValue){
Console.WriteLine(c);
Console.WriteLine(c.value);
}安全获取可空类型值
Console.WriteLine(c.GetValueOrDefault());
//0指定默认值
Console.WriteLine(c.GetValueOrDefault(100));
//100
自动判断是否为空
object o=null;
o?.ToString();
等价于
if(o!=null) o.ToString();
arrayInt?[0].ToString();
action?.Invoke();
- 空合并操作符
return left??right;
if(left!=null){
return left;
}else{
return right;
}
- 内插字符串$
- Console.WriteLine(“普通字符串{0},value”);
Console.WriteLine($”内插入字符串{value}”);
- 逐字字符串@
要实现这一切仅需要在字符串常量的值前加一个符号“@”,以这种形式赋值的字符串叫做逐字字符串,它后面的所有字符都被逐个地收录到字符串的值中!
因此,如果你需要类似“所见所得”效果的赋值,逐字字符串赋值方式会是你的首选!
此外,需要注意的是,当使用符号 “@” 为字符串赋值时,被赋值的所有字符将不需要经过转义——只有双引号这个本身作为界限的字符需要经过转义,此时它的转义输入方法是两个放在一起的双引号:””
在以这种方式给变量赋值时,也只有这唯一一个转义是合法的。如果希望诸如换行、制表符此类字符在字符串体现出来,也可以直接将带有换行与制表符的字面量字符串赋给字符串量;不过这样,就不能在换行前的前一行写注释或其他语句,否则这些语句将被当作字符串值的一部分。
在下面例子中的语句,可以实现不完成输入一系列不经转义的特殊字符:
(1) 逐字:
string NoEscapeFullFileName = @”C:\inetpub\ciznxcom"; // (此句中的反斜扛被直接赋值)
(2)双引号:
string StringWithDbQoute = @”Jim says,””he can reach home in about six minutes””.” //(此句中双引号被转义)
(3)换行: 相当于加一个回车\r 而不是\n
string StringWithNextLineChar = @”We can set a string value for a string variable
with such a “”NextLine”” char.”; // (此句中给字符串变量赋予了回车符,且未经任何转义;并且保持所有空格)
多线程(待更新)
知识点— 了解线程前先了解进程
进程(Process) 是计算机程序中关于某数据集合上的一次运行活动
是系统进行资源分配和调度的基本单位,是操作系统结构的基础
说人话:打开应用程序就是在操作系统上开启了一个进程
进程之间可以相互独立运行,互不干扰
进程之间也可以相互访问、操作
什么是线程
- 操作系统能够进行运算调度的最小单位。
它被包含在进程之中,是进程中的实际运作单位 - 一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程
- 我们目前写的程序 都在主线程中
简单理解线程:
- 就是代码从上到下运行的一条“管道”
什么是多线程
- 我们可以通过代码开启新的线程
- 可以同时运行代码的多条“管道”就叫多线程
在C#中,你可以通过多种方式创建和管理线程。以下是一些基本的线程创建和操作的示例代码:
迭代器
迭代器是什么
详解C# 迭代器 - Cat Qi - 博客园 (cnblogs.com)
- 迭代器(iterator)有时又称光标(cursor)
- 是程序设计的软件设计模式
- 迭代器模式提供一个方法顺序访问一个聚合对象中的各个元素
- 而又不暴露其内部的标识
- 在表现效果上看
- 是可以在容器对象(例如链表或数组)上遍历访问的接口
- 设计人员无需关心容器对象的内存分配的实现细节
- 可以用foreach遍历的类,都是实现了迭代器的
使用迭代器
IEnumerator,IEnumerable
命名空间: using System.Collections;
可以通过同时继承IEnumerable和IEnumerator实现其中的方法
1 | using System.Collections; |
- foreach 本质
- 先获取in 后面的对象的IEnumerator
- 会调用其中的GetEnumerator 方法
1 | class IterationSampleEnumerator : IEnumerator |
通过yield语句简化迭代
1 | public IEnumerator GetEnumerator() |