C# 关键字解析
C# 中的关键字解析
前言
最近在一个初创公司里做全栈开发。一开始以为去做
ASP.NET
(后端),结果微信小程序开发缺人 (汗)。反正闲着也是闲着,趁这个机会学习点C#
中的基础知识。
结合微软文档,对
C#
中的关键字进行解析。文章中,派生类默认为子类,基类默认为父类。
部分常见关键字省略: bool,break,case,char,continue,default,do,double,else,false,float,for,if,int,long,new,public,return,sizeof,string,true,try,typeof,void,while
关键字省略 (感觉可能用不到, 这里指的是日常哈): checked,extern,fixed,goto,implicit,in,lock,object,operator,sbyte,short,stackalloc,uint,ulong,unchecked,unsafe,ushort,volatile
1. abstract 关键字
在C#
中,关键字abstract
用于声明抽象类或类中的抽象成员。
抽象类特点:
- 抽象类不能实例化 (不能使用
new
关键字)。 - 抽象类可能包含抽象成员(方法、属性)和非抽象成员的实现。
- 只有抽象类中才允许抽象方法声明。
- 抽象类通常作为基类被子类继承,子类必须使用
override
实现抽象类中声明的抽象成员。
抽象方法特点:
- 凡是包含抽象方法的类都是抽象类。
- 抽象方法声明不提供实际的实现,因此没有方法主体。(抽象属性同理)
- 在抽象方法声明中不能使用
static
或virtual
关键字。- 抽象方法没有具体实现,所以不能使用
static
关键字来声明为静态方法。 - 抽象方法是隐式的虚拟方法,所以不需要
virtual
关键字。
- 抽象方法没有具体实现,所以不能使用
举个例子:
- 枪(抽象基类)是可以
Shoot
和Reload
(抽象方法)。 - 比如
AK47
和M4
(非抽象类子类),它们拥有Shoot
和Reload
的功能。 - 但具体是怎么
Shoot
和Reload
,AK47
和M4
是不一样的(具体实现)。
// 抽象类:Firearms |
为什么要用抽象类,而不是使用一般类class
或者接口interface
?
- 共享代码:抽象类可以定义一组相关类的通用行为和属性,并提供默认的实现。通过继承抽象类,子类可以继承和重用抽象类中的代码,减少代码的重复编写。这样可以提高代码的可维护性和扩展性。(这里指的是在
abstract class
里声明的非抽象方法) - 代码扩展性:抽象类可以包含抽象方法,这些方法在抽象类中没有具体的实现。通过继承抽象类并实现这些抽象方法,子类可以在具体的业务逻辑中实现自己的代码。这种方式使得代码具有扩展性,可以适应未来需求的变化。
- 部分实现:抽象类既可以包含抽象方法,也可以包含具体的实现。这使得抽象类可以提供一些默认的行为,同时也给子类提供了一定的灵活性。子类可以选择性地覆盖抽象类中的方法,或者直接使用抽象类中的实现。
- (待补充)
注意, 从
C# 8.0
开始interface
就可以在接口里提供默认的实现
2. as 关键字
在C#
中,关键字as
用于类型转换。
- 在实际应用中,通常会与
null
检查一起使用as
关键字。这是因为,如果转换不成功,as
会返回null
,而不是抛出异常。这使得as
关键字在面对类型转换失败时的行为更加温和。
public class Animal { ... } |
上面代码中,as
关键字尝试将animal
转换为Dog
类型。由于animal
对象实际上是一个Dog
类型的对象,所以转换成功。如果animal
对象是Animal
类型的对象,则上面代码会转换失败。
3. base 关键字
在C#
中,base
关键字用于在子类中引用基类的成员或调用基类的构造函数。
base
关键字只能用于子类,而不是基类中。- 如果基类成员是私有的,即使使用
base
关键字也无法访问。 - 在静态方法中使用
base
关键字将产生错误。 - 如果基类中有多个构造函数重载,子类在使用
base
关键字调用父类构造函数时,需要选择要调用的具体构造函数,并且提供与所选基类构造函数匹配的参数列表。
下面是base
关键字的几种常见用法
- 调用基类的构造函数
在派生类的构造函数中,可以使用base
关键字调用基类的构造函数。
public class BaseClass |
上面代码中,DerivedClass
继承自BaseClass
。在DerivedClass
的构造函数中,使用base(x)
调用了基类BaseClass
的构造函数,并传递参数x
。
- 引用基类的成员
当派生类中定义了一个与基类相同名称的成员变量或方法时,可以使用base
关键字来访问它。
public class BaseClass |
上面代码中,DerivedClass
继承自BaseClass
。在DerivedClass
中的SomeMethod
方法中,使用base.SomeMethod()
调用了基类BaseClass
的方法。
为什么要使用base
去调用基类的构造函数?
- 继承基类的行为和状态: 通过调用基类的构造函数,子类可以继承基类的行为和状态。基类可能包含一些重要的初始化逻辑,以确保它的成员和属性处于正确的状态。
- 提供基类所需的初始化参数: 如果基类的构造函数需要接收参数来进行初始化,派生类可以通过调用基类构造函数并传递适当的参数,来提供必要的信息。
- 避免冗余代码: 如果基类的构造函数已经包含了一些通用的、所有派生类都需要的初始化代码,那么在派生类中通过使用
base
关键字调用基类构造函数,可以避免重复编写这些初始化代码。 - (待补充)
4. byte 关键字
在C#
中,byte
关键字用于声明一个8
位无符号(unsigned
)整数类型的变量。
byte
关键字在C#
中具有多种用途和应用场景
- 存储和处理二进制数据:
byte
类型是一个8
位无符号整数,范围从0
到255
。因此,它非常适合用于存储和处理二进制数据,如图像、音频、视频、文件等。 - 网络编程: 在网络编程中,常常需要使用
byte
类型来读取和写入数据流、处理网络字节序等。 - 数据序列化和反序列化: 序列化是将对象转换为字节序列的过程,而反序列化则是将字节序列转换回对象。在数据序列化和反序列化过程中,
byte
类型通常用于表示和操作字节数据。 - …
举个例子:
// 读取图像文件的字节数据 |
5. 异常处理关键字
在C#
中,try
关键字用于定义一个try
块,表示其中可能会出现异常。
try
块后面通常跟随一个或多个catch
块,用于处理可能抛出的特定异常。最后还可以选择性地包含一个finally
块,它包含的代码无论是否抛出异常都会被执行。
using System; |
上面代码中,如果try
块中的代码引发了异常,程序会立即跳转到catch
块,寻找与异常类型匹配的catch
块,并执行相应的代码块。每个catch
块可以捕获并处理特定类型的异常。
- 处理异步函数的异常
异步函数通常返回一个Task
或Task<T>
对象。可以在try
块中await
这个任务,并在catch
块中处理可能的异常。
// 有可能抛出异常的异步操作 |
另外,不是所有的异常都应该被捕获。在大多数情况下,只应该捕获知道如何处理的异常。对于不知道如何处理的异常,最好让它们传播出去,这样调用者或者全局异常处理器可以捕获并处理它们。
(这里暂时省略处理迭代器方法中的异常,待补充)
6. class 关键字
在C#
中,class
关键字用于定义一个类。
- 类是一种引用类型,它定义了一组属性(字段、常量和事件)、方法和索引器的组合。类可以直接实例化,也可以作为基类继承。
class MyClass |
C#
中仅允许单一继承,即一个类仅能从一个基类继承实现。但是,一个类可拥有多个接口 (重要)。
继承 | 示例 |
---|---|
无 | class ClassA { } |
单一 | class DerivedClass : BaseClass { } |
无,实现两个接口 | class ImplClass : IFace1, IFace2 { } |
单一,实现一个接口 | class ImplDerivedClass : BaseClass, IFace1 { } |
下面是一些类的使用方法
(1) 访问修饰符: 类的访问修饰符决定了它在代码中的可见性。默认情况下,类是
internal
的,意味着它只在同一程序集中可见。类的成员默认是private
的,只能在类的内部访问。(2) 静态类: 使用
static
关键字可以定义静态类和静态成员。静态类不能实例化,不能被继承,且只能包含静态成员。静态成员只有一份,属于类本身,而不属于类的任何实例。
public static class MathUtils |
(3) 构造函数: 类可以有一个或多个构造函数,用于创建类的实例。构造函数的名称必须与类名相同,且不返回任何值。
(4) 静态构造函数: 用于初始化静态成员或执行静态初始化代码。它没有参数列表,也不能直接调用。静态构造函数在类的第一个实例或静态成员被访问之前自动调用,并且只执行一次。
访问静态成员时调用
public class MyClass |
创建类的实例时调用
public class MyClass |
- (5) 内部类: 内部类是定义在另一个类内部的类。内部类拥有访问外部类成员的特权,可以用于实现更复杂的逻辑和封装。(注意,虽然内部类可以访问其外部类的所有成员,但反过来不成立。)
class OuterClass |
- (6) 泛型类: 泛型类具有在实例化时可以指定参数类型的特性,从而提供了更灵活和类型安全的代码重用方式。
public class MyGenericClass<T> |
C#
还有许多其他不同的类,比如之前介绍的抽象类,还有接口,密封类,枚举类,部分类,特性类,委托类等等。(待补充)
7. const 关键字
在C#
中,const
关键字来声明某个常量字段或局部变量。常量字段和常量局部变量不是变量并且不能修改。
const int X = 0; |
8. decimal 关键字
在C#
中,当所需的精度由小数点右侧的位数决定时,decimal
关键字是合适的。
- 它是一种数据类型,用于存储精确的十进制数值(支持小数点后
28
的精确度),适用于需要高精度计算的场景,例如财务和货币计算。
decimal myDecimal = 3.14m; // 使用后缀 "m" 表示 decimal 类型 |
注意,decimal
类型虽然具有较高的精度和准确性,它的计算速度通常比其他浮点类型,比如 float
和 double
,慢。
9. delegate 关键字
在C#
中,delegate
关键字用于声明和使用委托。
它类似于一个装着方法的容器,可以将方法(实例方法,静态方法)作为对象进行传递,但前提是委托和对应传递方法的签名得是相同的,签名指的是他们的参数类型和返回值类型。(类似回调函数或者Thunk
函数)
// 委托的声明,指定了返回值类型为 int,接收两个 int 类型的参数。 |
注意,在C#
中创建委托对象时,并不一定需要使用new
关键字来实例化委托。
- 使用
new
关键字和委托类型的构造函数创建委托对象
MyDelegate myDelegate = new MyDelegate(ShowMessage); |
在这种情况下,我们使用new MyDelegate
语法创建了一个委托对象myDelegate
,并将ShowMessage
方法绑定到委托上。
- 使用隐式方法组转换来创建委托对象
MyDelegate myDelegate = ShowMessage; |
在这种情况下,我们直接将方法名ShowMessage
赋值给委托对象myDelegate
,编译器会自动进行隐式方法组转换,将方法转换为委托对象。
- 使用匿名方法创建委托对象
MyDelegate myDelegate = delegate(string message) |
匿名方法的语法是使用关键字delegate
后跟一个参数列表和方法体。
- 使用
Lambda
表达式创建委托对象
// Lambda表达式的参数部分只使用了参数名"message",而没有指定参数类型。 |
在这种情况下,我们使用Lambda
表达式定义了一个匿名方法,将其赋值给委托对象myDelegate
。
那什么情况下应该使用委托
delegate
?(1) 事件处理: 委托广泛用于事件处理模型中。通过定义委托类型和事件,可以将事件与特定的处理方法关联起来。当事件发生时,委托会调用绑定的方法,从而实现事件的处理和响应。
// 声明一个委托 |
- (2) 回调函数: 委托可用作回调函数的一种方式。当一个方法需要在完成后通知另一个方法时,可以将委托作为参数传递给该方法,并在适当的时候调用委托以执行回调操作。
public delegate void MyCallbackDelegate(string message); |
- (3) 多播委托 (委托链): 委托还支持多播(
multicast
)的功能,即一个委托可以绑定多个方法。这使得可以将多个方法作为委托的调用列表,并按顺序依次调用它们。
delegate void MyDelegate(); |
- (4) 泛型委托:
C#
中的委托支持泛型,可以更好地支持类型安全性和代码重用。
delegate T MyGenericDelegate<T>(T value); |
- (5)
LINQ
和Lambda
表达式: 在LINQ
中,委托用于定义查询操作。而Lambda
表达式实质上是一种特殊的委托,以一种更简洁和强大的方式定义匿名函数。
using System; |
注意,在C#
中,不一定非要使用delegate
关键字来定义委托。其实,Func<T, TResult>
、Action<T>
等都是内置的委托类型,你可以直接使用它们,而无需使用delegate
关键字。(C#
中内置的委托类型)
- (6) 定制或扩展方法: 在
C#
中,可以利用委托去定制或扩展方法,改变其行为或增加新的功能。例如,可以使用委托来定制比较方法,从而对集合进行自定义排序。
public delegate int Comparison<in T>(T x, T y); |
- (7)(待补充)
10. enum 关键字
在 C#
中,enum
关键字用于声明枚举类型。
枚举是一种值类型,它表示一个固定的值集合。这些值被称为枚举成员,并且每一个都有一个关联的常量值。默认情况下,第一个枚举成员的值是
0
,后续的成员值依次递增1
,但是可以显式地改变这个顺序。假设有一个枚举类型定义了一周的天数
public enum DayOfWeek |
- 可以创建一个变量来存储当前的星期几
DayOfWeek today = DayOfWeek.Monday; |
- 可以将枚举值转换为其他类型,例如整数或字符串
int dayNumber = (int)DayOfWeek.Friday; // 5 |
- 反过来,也可以将其他类型转换为枚举值
DayOfWeek day = (DayOfWeek)5; // DayOfWeek.Friday |
如果枚举表示的是一组位标志,那么应该使用[Flags]
属性,并为枚举成员指定2
的幂次方值。这样就可以使用位运算符来组合、添加或移除枚举值。
[ ] |
在这个例子中,AccessRights
枚举定义了一组访问权限,每个权限是二进制位的一个标志。这允许我们将不同的权限组合到一起,然后通过位运算检查特定的权限是否存在。
// 设置读和写权限 |
除了位运算符的使用,也可以直接使用HasFlag
方法来检查特定的权限是否存在。
AccessRights rights = AccessRights.Read | AccessRights.Write; |
11. event 关键字
在C#
中,event
关键字用于声明一个事件。(如何发布符合 .NET 准则的事件)
- 事件是由对象在特殊情况下触发的一种机制,例如用户点击了一个按钮,或者某个操作已经完成。对象可以订阅这个事件,这样当事件发生时,就会调用订阅者定义的事件处理程序。
在事件的定义中,通常会有两部分组成。一部分是声明事件的委托(delegate
),另一部分是事件本身。下面是一个简单的event
使用例子
// 定义一个委托 |
上面例子可以看出,事件帮助了事件发布者和订阅者之间的解耦。发布者并不需要知道谁订阅了它的事件,也不需要知道事件将如何被处理。这使得我们可以独立地修改发布者和订阅者的代码,而不需要担心它们之间的依赖关系。
event
关键字使用注意点(1) 避免内存泄漏
- 如果一个对象订阅了另一个对象的事件,那么在不需要订阅事件时应当及时取消订阅,以避免发生内存泄漏。(只要事件发布者还在内存中,订阅者就不会被垃圾回收,即使订阅者已经不再使用)
public class Subscriber |
- (2) 委托类型
System.EventHandler
和System.EventHandler<T>
是标准的事件委托类型,推荐在事件定义中使用这些类型以提高代码的可读性和一致性。
public class Publisher |
- (3) 事件访问器
- 事件访问器允许在事件被添加或移除时执行自定义的逻辑。这对于调试或管理订阅者列表非常有用。
using System; |
12. explicit 关键字
在C#
中,explicit
关键字用于类型转换。
explicit
关键字通常用在用户定义的类型转换操作中,允许从一种数据类型转换到另一种数据类型,例如从类到接口,或者从父类到子类等等。在某些情况下,转换操作可能导致数据丢失或抛出异常。使用
explicit
关键字可以强制开发者明确地处理转换,从而减少在转换过程中出现错误的可能性。
下面是一个explicit
关键字的例子
public class Fahrenheit |
上面例子中,不能直接将Fahrenheit
对象转换为Celsius
,必须明确地进行转换。
13. foreach 关键字
在C#
中,foreach
关键字用于遍历集合(如数组,列表等)中的元素。
- 它的工作原理是通过调用集合对象的
GetEnumerator
方法来获取IEnumerator
或IEnumerator<T>
对象,然后在每次迭代中调用MoveNext
方法和Current
属性。
注意,后面
IEnumerator
和IEnumerator<T>
统称为枚举器
var fibNumbers = new List<int> { 0, 1, 1, 2, 3, 5, 8, 13 }; |
下面是一些foreach
关键字的使用方法
- (1) 实现枚举器接口的集合
在C#
中,许多内置集合类型都实现了枚举器接口,这使得它们可以使用foreach
循环进行迭代。
Dictionary<string, int> dictionary = new Dictionary<string, int> |
- (2) 实现枚举器接口的自定义类
如果想要自定义类能够被foreach
循环遍历,需要实现枚举器接口。这个接口定义了一个方法,GetEnumerator
,返回一个实现了枚举器接口的对象。
public class MyCollection : IEnumerable<int> |
- (3) 异步流的迭代
异步流的迭代是C# 8.0
新增的特性,这使得我们能够在处理异步操作时,写出更高效和更易于理解的代码。异步流的迭代通过await foreach
关键字实现。它能够异步地迭代这样的序列,而不会阻塞主线程。
public async IAsyncEnumerable<int> GenerateSequence() |
在上述示例中,GenerateSequence
方法生成了一个异步序列。每个元素都是在等待一段时间后生成的,以模拟异步操作。TestAsyncStream
方法使用await foreach
语句来异步地迭代这个序列。每次迭代都会异步地等待下一个元素。
14. interface 关键字
在 C#
中,接口interface
是一种定义行为契约的类型。接口声明一组方法、属性、事件或索引器,但不提供其实现。类class
或结构体struct
可以实现一个或多个接口,并提供接口中定义的成员的具体实现。
public interface IAnimal |
- 类或结构体实现接口时,必须提供接口中定义的所有成员的具体实现。
interface IPoint |
- 接口可以从一个或多个其他接口继承。实现该接口的类必须实现所有继承的接口成员。
public interface IDrawable |
- 接口与抽象类的区别
- 实现: 接口中不能包含任何实现(
C# 8.0
的默认接口方法除外),而抽象类可以包含实现。 - 多重继承: 一个类可以实现多个接口,但只能继承一个抽象类。
- 构造函数: 接口不能有构造函数,而抽象类可以。
- 成员类型: 接口中不能包含字段(成员变量),而抽象类可以。
- 实现: 接口中不能包含任何实现(
public interface IShape |
public interface IShape |
15. internal 关键字
在C#
中,internal
关键字用于指定成员的可访问性。internal
修饰符使成员在同一程序集assembly
内可访问,但在程序集外不可访问。(这里的程序集不是指同一个文件, 也不是指namespace
)
程序集
Assembly
: 程序集是.NET
中的基本部署单元,可以是一个DLL
或EXE
文件。一个程序集包含一个或多个命名空间和类型,包含了元数据和代码。程序集是.NET
中的物理边界。类的
internal
可访问性- 在以下示例中,
MyClass
类被声明为internal
,因此它只能在同一程序集内访问。
- 在以下示例中,
// 文件1:MyClass.cs |
在上面的示例中,MyClass
类和它的Display
方法都可以在Program
类中访问,因为它们在同一个程序集内。
- 方法的
internal
可访问性- 在以下示例中,
MyClass
类是public
的,但其中的InternalMethod
方法被声明为internal
,因此它只能在同一程序集内访问。
- 在以下示例中,
// 文件1:MyClass.cs |
在上面的示例中,MyClass
的InternalMethod
方法可以在Program
类中访问,因为它们在同一个程序集内。
- 跨程序集访问
- 如果尝试从另一个程序集访问
internal
成员,会导致编译错误。例如,假设有两个项目:ProjectA
和ProjectB
。ProjectA
定义了一个internal
类,而ProjectB
试图访问它。
- 如果尝试从另一个程序集访问
// ProjectA - 文件1:MyClass.cs |
在这个示例中,由于MyClass
被声明为internal
,所以它不能在ProjectB
中访问,会导致编译错误。
- 选择
internal
而不选择public
的原因- 封装实现细节: 当你希望封装类、方法或其他成员,只让它们在同一个程序集内使用,而不暴露给外部程序集时,可以使用
internal
修饰符。 - 控制
API
公开性: 在设计库或框架时,internal
修饰符可以帮助控制哪些类型和成员应暴露给库的使用者,哪些应保持内部使用。
- 封装实现细节: 当你希望封装类、方法或其他成员,只让它们在同一个程序集内使用,而不暴露给外部程序集时,可以使用
16. is 关键字
is
关键字在C#
中用于检查对象是否是特定类型的实例。
- 类型检查
- 使用
is
可以检查对象是否是特定类型的实例。如果是,则返回true
,否则返回false
。
- 使用
object obj = "Hello, World!"; |
- 模式匹配
- 从
C# 7.0
开始,is
关键字还支持模式匹配。这使得检查类型和转换类型可以在一个操作中完成。
- 从
object obj = "Hello, World!"; |
在这个示例中,如果obj
是string
类型,is
关键字不仅检查类型,还将obj
转换为string
并赋值给变量s
。然后可以直接使用s
。
- 使用
is
进行null
检查is
关键字还可以用来检查对象是否为null
。
object obj = null; |
- 检查接口实现
is
关键字也可以用于检查对象是否实现了某个接口。
public interface IAnimal |
17. namespace 关键字
在C#
中,namespace
(命名空间) 是用于组织代码和防止命名冲突的一种机制。命名空间提供了一种逻辑上的分组方式,使得开发者可以更容易地管理和维护代码。
- 定义命名空间
namespace MyApplication |
在这个示例中,MyClass
类被定义在MyApplication
命名空间中。
- 使用命名空间
- 要使用某个命名空间中的类型,可以通过
using
关键字导入命名空间。
- 要使用某个命名空间中的类型,可以通过
using MyApplication; |
- 命名空间的好处
- 组织代码: 通过使用命名空间,可以将相关的类、接口、枚举等进行分组,增加代码的可读性和维护性。
- 避免命名冲突: 在大型项目中,不同模块或库中可能会有同名的类型,通过使用命名空间,可以避免这些冲突。
18. null 关键字
null
关键字在C#
中表示对象引用不指向任何实例。它是引用类型的默认值,意味着变量不引用任何对象。
- 常见用途 (省略部分常见情况)
- (1) 空合并运算符
- 空合并运算符
(??)
提供了简洁的语法来处理null
值。如果左操作数为null
,则返回右操作数。
- 空合并运算符
string str = null; |
- (2) 空条件运算符
- 空条件运算符
(?.)
在访问成员或调用方法时检查null
。如果对象为null
,表达式返回null
而不引发异常。
- 空条件运算符
MyClass obj = null; |
- (3) 处理值类型的
null
- 对于值类型,不能直接赋值
null
。但是,可以使用可空类型(Nullable<T>
或T?
)来表示可以为null
的值类型。
- 对于值类型,不能直接赋值
int? nullableInt = null; |
19. out 关键字
在C#
中,out
关键字用于方法参数,表示该参数将由方法初始化并返回给调用方。out
参数在方法调用时不需要被初始化,但在方法返回之前必须被赋值。
- 定义和使用
out
参数- 在方法声明中使用
out
关键字,并在方法调用时也使用out
关键字。
- 在方法声明中使用
public class Program |
- 使用
out
返回多个值- 通过
out
参数,可以让方法返回多个值。
- 通过
public class Program |
out
与ref
的区别- 初始化要求: 使用
out
参数的方法不要求在调用前初始化该参数,而ref
参数必须在调用前初始化。 - 赋值要求: 使用
out
参数的方法必须在方法返回之前为该参数赋值,而ref
参数不强制要求在方法内赋值。
- 初始化要求: 使用
以下限制适用于使用
out
关键字- 异步方法中不允许使用
out
参数。 - 迭代器方法中不允许使用
out
参数。 - 属性不能作为
out
参数传递。
- 异步方法中不允许使用
20. override 关键字
override
关键字在C#
中用于方法、属性、索引器或事件,表示派生类中重写基类的虚方法、虚属性、虚索引器或虚事件。通过使用override
,派生类可以提供基类成员的新实现。
- 定义基类和派生类
- 在基类中,使用
virtual
关键字定义可以被重写的成员。在派生类中,使用override
关键字重写这些成员。
- 在基类中,使用
public class BaseClass |
- 使用
override
重写属性和方法- 可以使用
override
关键字重写基类中的虚属性。(重写方法如上)
- 可以使用
public class BaseClass |
- 注意事项
- 虚成员: 只有基类中标记为
virtual
或abstract
的成员才能被重写。 - 访问修饰符: 重写的成员的访问级别必须与基类中的成员一致,或者更严格。例如,如果基类中的虚方法是
public
,则派生类中的重写方法也必须是public
,不能是protected
或private
。
- 虚成员: 只有基类中标记为
21. params 关键字
params
关键字在C#
中用于指定一个方法参数数组,使得方法可以接受可变数量的参数。
- 使用
params
关键字的参数必须是方法的最后一个参数,并且它可以接受任意数量的参数,包括零个参数。
public void PrintNumbers(params int[] numbers) |
- 使用
params
的理由 - (1) 灵活性和简洁性
params
关键字提供了在方法调用时的灵活性和简洁性。它允许调用者传递任意数量的参数,而不需要显式地创建一个数组。这使得方法调用更简洁、更直观。
public void PrintNumbers(int[] numbers) |
如果不使用params
关键字,则需要显式地创建数组
- (2) 支持多参数重载
params
关键字允许方法支持多种参数重载,这在需要处理多个不同参数数量的情况下非常有用。它避免了为每种可能的参数数量定义多个重载方法。
public void DisplayItems(params string[] items) |
22. private 关键字
private
关键字是C#
中的访问修饰符,用于控制类或结构体成员的可访问性。标记为private
的成员只能在定义它们的类或结构体内部访问。
- 使用
private
关键字可以隐藏类的实现细节,限制对类内部数据的直接访问,从而提高代码的封装性和安全性。
class Employee2 |
- 注意事项
private
是C#
中最严格的访问修饰符,仅限于在类或结构体内部访问。private
成员不能在派生类中访问,即使是通过继承。- 在结构体中,所有成员默认为
private
,而在类中,成员默认为private
。
23. protected 关键字
protected
关键字是C#
中的访问修饰符,用于控制类或结构体成员的可访问性。标记为protected
的成员只能在以下两种情况下访问:
- 在定义该成员的类内部访问
- 在派生类中访问
这种访问修饰符允许你在保持数据封装的同时,为派生类提供访问基类成员的能力。
public class Animal |
- 使用
protected
的原因- 继承与扩展:
protected
关键字允许派生类访问基类的成员,从而支持类的继承和扩展。 - 数据封装: 通过限制成员的访问权限,
protected
关键字有助于保护类的内部状态,同时允许派生类进行必要的访问。
- 继承与扩展:
以下是对protected internal
和private protected
访问修饰符的详细解释
protected internal
关键字结合了protected
和internal
的特性,成员可以在以下两种情况下访问- 同一程序集:
internal
允许同一程序集中的任何代码访问。 - 派生类:
protected
允许派生类访问,即使它们在不同的程序集中。
- 同一程序集:
因此,protected internal
的成员可以被同一程序集中的所有代码访问,也可以被其他程序集中派生类访问。下面是一个例子
- 图形库
Assembly: GraphicsLibrary.dll
:Shape.cs
namespace GraphicsLibrary |
- 图形库
Assembly: GraphicsLibrary.dll
:Circle.cs
namespace GraphicsLibrary |
- 使用图形库的应用程序
Assembly: GraphicsApp.exe
:Program.cs
using GraphicsLibrary; |
总而言之,protected internal
允许灵活地在同一程序集内访问共享的实现细节,同时保留跨程序集继承的能力。
private protected
关键字结合了private
和protected
的特性,成员只能在以下两种情况下访问- 同一类内部:
private
允许在同一类内部访问。 - 同一程序集中派生类:
protected
允许在派生类中访问,但前提是这些派生类在同一程序集中。
- 同一类内部:
因此,private protected
的成员只能被定义它们的类及其同一程序集中的派生类访问,不能被其他程序集的派生类访问。下面是一个例子
- 订单系统
Assembly: OrderSystem.dll
:Order.cs
namespace OrderSystem |
- 订单系统
Assembly: OrderSystem.dll
:SpecialOrder.cs
namespace OrderSystem |
- 外部应用程序
Assembly: ExternalApp.dll
:Program.cs
using OrderSystem; |
总而言之,private protected
提供了一种更严格的访问控制,防止不同程序集中的类访问某些内部细节,这在保护敏感数据或内部实现细节时特别有用。
24. readonly 关键字
readonly
关键字在C#
中用于修饰字段,表示该字段在初始化后不能被更改。readonly
字段只能在以下情况下赋值
- 字段声明时: 可以在声明字段时直接赋值。
- 构造函数中: 可以在类的构造函数中赋值,包括实例构造函数和静态构造函数。
readonly
字段与const
常量的不同之处在于,const
是编译时常量,必须在声明时赋值,并且其值在编译时就确定,而readonly
字段可以在运行时被赋值,并且可以根据构造函数的逻辑赋予不同的值。
public class Person |
const
和readonly
的区别const
适用于值在编译时就确定不变的情况,如数学常量pi
。readonly
适用于值在运行时确定且一旦初始化后不再改变的情况,如配置文件中的某些设置。
public class Constants |
25. ref 关键字
ref
关键字在C#
中用于方法参数,表示参数以引用方式传递。这意味着在方法内对参数的任何修改都会影响到调用方传递的实际变量。使用ref
关键字的要求
- 在方法声明中使用: 必须在方法的参数列表中使用
ref
关键字。 - 在方法调用时使用: 调用方法时也必须在实际参数前加上
ref
关键字。 - 初始化要求: 传递给
ref
参数的变量必须在调用方法之前初始化。
public class Program |
如果上面的示例中没有使用ref
,那么第二个Console
的输出就还会是5
。原因是方法接收参数的副本,方法内对参数的修改不会影响到外部的实际变量。
- 使用
ref
的场景- 需要修改调用方变量的场景: 当你希望在方法内修改调用方传递的变量时,可以使用
ref
关键字。例如,交换两个变量的值。 - 需要返回多个值的场景: 虽然方法本身只能返回一个值,但可以通过
ref
或out
参数返回多个值。
- 需要修改调用方变量的场景: 当你希望在方法内修改调用方传递的变量时,可以使用
public class Program |
ref
与out
的区别- 初始化要求: 传递给
ref
参数的变量必须在调用方法之前初始化。传递给out
参数的变量可以在方法调用之前未初始化,但必须在方法内部赋值。 - 主要用途:
ref
用于传递数据给方法,并且希望方法内部修改该数据。out
用于方法返回多个值。
- 初始化要求: 传递给
26. sealed 关键字
sealed
关键字在C#
中用于类和方法,提供了不同的功能
- 用于类: 防止其他类从它派生。也就是说,
sealed
类不能作为基类,无法被继承。 - 用于方法: 当用在基类中的虚方法上时,防止派生类进一步重写该方法。
当一个类被标记为sealed
时,它不能被继承。这通常用于防止不希望的继承,或者当类的设计不需要被扩展时。使用sealed
类可以提高性能,因为它的成员在编译时可以进行更多的优化。
public sealed class FinalClass |
当一个类继承自基类,并且重写了基类中的虚方法时,可以使用sealed
关键字阻止进一步的重写。这种情况下,sealed
关键字必须与override
一起使用。
public class BaseClass |
- 使用
sealed
的场景- 提高安全性: 通过防止类的继承,可以保护类的实现细节,防止子类意外地改变类的行为。
- 性能优化: 标记为
sealed
的类和方法可以进行更多的编译时优化,因为编译器知道这些类和方法不会被重写。 - 限制扩展性: 当一个类的设计不适合扩展时,可以使用
sealed
防止进一步的继承。
27. static 关键字
static
关键字在C#
中用于声明类成员和类本身。它表示这些成员或类与具体实例无关,而是属于类型本身。
- 静态类
static class
- 定义: 静态类是不能被实例化的类,所有成员都必须是静态的。
- 特性: 静态类通常用于组织一组相关的静态方法和常量,不允许创建静态类的实例。
- 用途: 常用于实用类
Utility class
或库,提供静态方法进行通用操作。
public static class MathUtilities |
- 静态类
static
成员- 静态成员包括静态字段、静态属性、静态方法、静态事件等。它们属于类本身,而不是类的具体实例。
- 静态字段: 所有实例共享的字段,可以用于存储全局状态或常量。
- 静态属性: 提供对静态字段的访问,通常用于封装。
- 静态方法: 可以在不创建类实例的情况下调用,用于执行与类实例无关的操作。
- 静态构造函数: 用于初始化静态字段或执行其他静态初始化,只在类首次被访问时调用一次。
public class Counter |
- 静态构造函数
static constructor
- 静态构造函数用于初始化静态字段或执行类级别的初始化。它在类的第一次使用时被调用,且只调用一次。
- 特性: 没有访问修饰符,因为它不能由外部代码调用;不接受参数,因为它是自动调用的。
- 用途: 通常用于初始化静态数据。
public class Example |
static
关键字在C#
中非常重要,用于定义不依赖于具体对象实例的类和成员。它提供了一个全局的、共享的访问点,适用于需要统一管理状态或提供通用操作的场景。
28. struct 关键字
struct
关键字在C#
中用于定义值类型。值类型与引用类型(如类)不同,值类型在内存中直接存储其数据,而不是存储对数据的引用。结构体是一种轻量级的数据结构,通常用于表示小型数据对象。
- 内存分配: 值类型的数据通常分配在栈上,而不是堆上。
- 数据存储: 值类型直接包含其数据,而引用类型包含对数据的引用。
- 无继承: 结构体不能继承自其他结构体或类,也不能被继承。但它们可以实现接口。
- 无参数的构造函数: 不能定义无参数的构造函数,因为默认的无参数构造函数始终存在,用于初始化字段为其默认值。
public struct Point |
struct
的适用场景- 轻量级数据结构: 结构体适合用于表示轻量级的数据对象,如二维坐标、颜色、时间等。
- 不可变对象: 因为结构体是值类型,通常可以设计成不可变对象,提供更高的安全性和一致性。
- 性能考虑: 由于结构体在栈上分配,访问速度较快,适合于频繁分配和释放的小型对象。
29. switch 关键字
这里主要介绍
switch
表达式
switch
表达式是C# 8.0
引入的一种简洁且功能强大的分支选择机制。与传统的switch
语句不同,switch
表达式不仅能根据输入值进行分支,而且更灵活地结合了模式匹配的特性。
var result = expression switch |
switch
表达式提供了一种简洁且灵活的方式来进行条件分支选择,与传统switch
语句相比,它不仅减少了代码量,还提高了代码的可读性和维护性。
30. this 关键字
this
关键字在C#
中用于引用当前实例的成员,包括字段、方法、属性、事件和索引器。它在类的非静态成员中非常有用。此外,this
关键字还有其他特定用途,如调用构造函数和扩展方法。
- 引用当前实例的成员
- 在类的实例方法或属性中,可以使用
this
关键字引用当前实例的成员,尤其是在成员名称与方法参数名称相同时。
- 在类的实例方法或属性中,可以使用
public class Person |
- 调用其他构造函数
this
关键字可以在构造函数中调用同一个类的其他构造函数,称为构造函数重载。
public class Rectangle |
- 扩展方法的调用
this
关键字在扩展方法的定义中用于指示被扩展的类型。
public static class StringExtensions |
在这个示例中,IsNullOrEmpty
方法是一个扩展方法,它使用this
关键字将string
类型扩展为包含该方法的类型。this
关键字指示value
参数为被扩展的类型的实例。
31. throw 关键字
throw
关键字在C#
中用于引发异常。异常是一种在程序执行过程中出现的错误或意外情况,使用throw
可以中断程序的正常执行流程,并转到相关的异常处理代码。
- 引发异常
- 使用
throw
关键字可以显式引发一个异常,通常在检测到程序状态不合法或出现错误时使用。
- 使用
public class Program |
- 重新引发异常
- 在捕获异常后,可能希望重新引发异常以允许外部代码进一步处理。在
catch
块中使用throw
可以重新引发当前捕获的异常。
- 在捕获异常后,可能希望重新引发异常以允许外部代码进一步处理。在
using System; |
- 注意事项
- 使用异常类型:
throw
关键字后面必须跟一个异常对象。异常对象通常是从System.Exception
类派生的类实例。 - 重新引发的正确方式: 在
catch
块中重新引发异常时,使用throw
; 而不是throw ex
;。前者保留了原始异常的堆栈跟踪信息,而后者会重置堆栈跟踪。
- 使用异常类型:
catch (Exception ex) |
32. using 关键字
using
关键字在C#
中有两种主要的用法
- 资源管理 (
using
语句): 用于定义一个范围,在该范围结束时会自动释放特定的资源。通常用于处理非托管资源,如文件、数据库连接等,需要显式释放的资源。 - 命名空间导入 (
using
指令): 用于导入命名空间,以便在代码中可以简化对命名空间内类型的引用。
资源管理 (using
语句)
using
语句用于确保实现了IDisposable
接口的对象在使用完后被正确地释放。IDisposable
接口包含一个Dispose
方法,用于释放资源。
using System; |
命名空间导入 (using
指令)
using
指令用于在文件的顶部导入命名空间,以便在代码中简化对该命名空间内类型的引用。这可以减少代码中的冗长类型名称,并提高代码的可读性。
using System; |
别名: 可以使用using
指令为类型或命名空间创建别名。
using Project = MyCompany.ProjectNamespace; |
33. virtual 关键字
virtual
关键字在C#
中用于修饰方法、属性、事件或索引器,表示这些成员可以在派生类中被重写。
使用
virtual
关键字定义的成员提供了一个默认的实现,但允许派生类通过override
关键字提供新的实现,从而改变基类的行为。虚方法
Virtual Methods
- 虚方法是指在基类中定义,并允许派生类重写的方法。虚方法在基类中提供了默认实现,但派生类可以通过重写它来提供新的行为。
public class BaseClass |
- 虚属性
Virtual Properties
- 虚属性类似于虚方法,它们允许在派生类中重写基类中的属性。
public class BaseClass |
- 虚事件
Virtual Events
- 虚事件与虚方法类似,允许派生类重写事件的订阅
add
和取消订阅remove
行为。
- 虚事件与虚方法类似,允许派生类重写事件的订阅
using System; |
virtual
的使用场景- 多态性: 虚方法和属性允许派生类重写基类的实现,从而实现多态性。基类的方法可以调用派生类的实现,而无需了解派生类的具体类型。
- 默认行为: 虚方法和属性可以提供基类的默认行为,派生类可以选择接受默认行为或提供自己的实现。
为什么选择virtual
而不是abstract
(1)
virtual
关键字- 定义:
virtual
关键字用于定义可以在派生类中重写的成员。虚成员在基类中有一个默认的实现,派生类可以选择重写该实现。 - 用途: 当基类提供一个默认的实现,但派生类可以根据需要覆盖该实现时,使用
virtual
。它允许提供一个基础行为,并在派生类中进行扩展或修改。
- 定义:
(2)
abstract
关键字- 定义:
abstract
关键字用于定义一个没有实现的成员,派生类必须提供该成员的具体实现。抽象成员没有方法体,类本身也必须是抽象的abstract class
。 - 用途: 当基类强制派生类实现某些方法或属性时,使用
abstract
。它用于定义一个抽象的接口或行为规范,所有的派生类都必须实现这些行为。
- 定义:
public abstract class Animal |
34. get 和 set 关键字
get
和set
关键字在C#
中用于定义属性的访问器。
get
访问器用于返回属性的值,而set
访问器用于为属性分配新值。
public class Person |
- 自动属性
- 自动属性是一种简化语法,它让编译器自动生成私有的后备字段。使用自动属性时,不需要显式定义
get
和set
访问器的实现。
- 自动属性是一种简化语法,它让编译器自动生成私有的后备字段。使用自动属性时,不需要显式定义
public class Person |
- 只读和写入属性
- 只读属性: 只定义
get
访问器的属性。 - 写入属性: 只定义
set
访问器的属性。
- 只读属性: 只定义
// 只读属性 |
- 属性的访问控制
- 可以为属性的
get
和set
访问器指定不同的访问修饰符,以控制它们的访问级别。
- 可以为属性的
public class Person |
在这个示例中,Name
属性的set
访问器是私有的,因此只能在Person
类内部设置Name
的值,而get
访问器是公有的,允许从外部读取Name
的值。
35. add 和 remove 关键字
add
和remove
关键字在C#
中用于自定义事件的访问器。这两个关键字允许你定义事件处理程序的添加和移除逻辑,它们通常与event
关键字一起使用。
using System; |
通过自定义add
和remove
访问器,开发者可以完全控制事件的订阅和取消订阅过程,使得事件系统更加灵活和可控。
36. yield 语句
yield
语句在C#
中用于实现迭代器,该语句使一个方法、get
访问器或operator
返回一个序列的元素而不必创建临时的集合。这对于在需要延迟执行或处理大数据集合时非常有用,因为它允许按需生成序列中的元素。
基本概念
- 迭代器: 一个对象,它实现了
IEnumerable
或IEnumerator
接口,提供一种逐一访问序列中元素的方法,而无需暴露底层表示。 - 延迟执行:
yield
语句使得方法的执行在每次返回一个元素后可以暂停,并在下一次需要元素时恢复。这种特性被称为延迟执行。
- 迭代器: 一个对象,它实现了
基本用法
yield return
和yield break
yield return
: 用于返回一个序列中的下一个元素,并暂停方法的执行,直到序列中的下一个元素被请求。yield break
: 用于终止迭代,立即退出迭代器块。
using System; |
yield
的优点- 简化代码: 使用
yield
可以轻松地创建返回多个值的迭代器,而无需创建临时集合或手动实现IEnumerable
或IEnumerator
接口。 - 节省内存: 由于
yield
语句支持延迟执行,它只在需要时生成元素,避免了一次性加载所有数据,节省了内存。 - 提高性能: 在处理大数据集或耗时的计算时,
yield
可以提高性能,因为它允许在序列中逐一访问元素,而不是一次性生成所有元素。
- 简化代码: 使用
37. async 关键字
async
关键字在C#
中用于标识一个方法、lambda
表达式或匿名方法是异步的。异步方法允许你在不阻塞调用线程的情况下执行长时间运行的操作(如I/O
操作、网络请求等)。这种机制使得应用程序可以保持响应,而不需要为每个长时间运行的任务创建新线程。
- 基本概念
- 异步方法: 使用
async
关键字修饰的方法,它可以包含一个或多个await
表达式。当异步方法执行到await
表达式时,它会异步地等待操作完成,而不阻塞当前线程。 - 返回类型: 异步方法通常返回
Task
或Task<T>
对象,表示一个异步操作的结果。如果方法没有返回值,则返回Task
;如果方法有返回值,则返回Task<T>
,其中T
是返回值的类型。
- 异步方法: 使用
using System; |
- 重要注意事项
- 异步所有: 异步方法内部调用的所有可能阻塞的操作也应尽可能使用异步版本。
- 错误处理: 在异步方法中,可以像处理同步代码中的异常一样使用
try-catch
进行异常处理。未处理的异常会在返回的Task
对象上引发。
38. lambda 表达式
Lambda
表达式是C#
中的一种匿名函数,它可以包含表达式或语句块,并且可以用来创建委托或表达式树类型。Lambda
表达式提供了一种简洁的语法,用于定义内联的函数或将函数作为参数传递给方法。Lambda
表达式通常用于LINQ
查询、事件处理程序和函数式编程。
- 使用单行表达式的
Lambda
表达式
Func<int, int, int> multiply = (x, y) => x * y; |
在这个示例中,multiply
是一个Func<int, int, int>
类型的委托,它接受两个int
参数并返回一个 int
。Lambda
表达式(x, y) => x * y
定义了这个委托的实现。
- 使用语句块的
Lambda
表达式
Action<string> greet = name => |
在这个示例中,greet
是一个Action<string>
类型的委托,它接受一个string
参数并返回void
。Lambda
表达式name => { ... }
使用了语句块来执行多行操作。
- 无参数的
Lambda
表达式
Func<int> getRandomNumber = () => new Random().Next(1, 100); |
在这个示例中,getRandomNumber
是一个Func<int>
类型的委托,它不接受任何参数并返回一个int
。Lambda
表达式() => new Random().Next(1, 100)
定义了一个没有参数的函数。
- 单个参数的简写形式
Func<int, int> square = x => x * x; |
在这个示例中,square
是一个Func<int, int>
类型的委托,它接受一个int
参数并返回一个int
。Lambda
表达式x => x * x
省略了参数类型和括号。
LINQ
查询Lambda
表达式广泛用于LINQ
查询中,作为查询表达式的条件和投影。
var numbers = new List<int> { 1, 2, 3, 4, 5 }; |
- 委托和事件处理
Lambda
表达式可以用来简洁地定义委托和事件处理程序。
Button btn = new Button(); |
39. 匿名方法
匿名方法Anonymous Methods
是C#
中的一种定义委托实例的方式,它允许你在代码中直接声明一个没有名称的方法。匿名方法提供了一种简洁的方式来创建内联的委托,而不必显式地定义一个单独的方法。
- 匿名方法使用
delegate
关键字,并且可以包含参数和方法体。
using System; |
- 匿名方法和
Lambda
表达式比较
// 匿名方法 |
- 何时使用匿名方法
- 复杂逻辑: 当方法体包含复杂逻辑或多行代码时,可以选择使用匿名方法。
- 捕获多个变量: 需要捕获多个外部变量或参数时,匿名方法可以提供更清晰的语法结构。
40. 泛型函数
泛型函数Generic Functions
是指可以接受不同类型参数的函数。C#
中的泛型函数通过在函数的定义中使用类型参数,使得函数可以独立于具体的数据类型来编写。在调用泛型函数时,具体的类型会被替换为实际使用的类型。这样,泛型函数可以在不重复编写代码的情况下适应多种数据类型。
- 定义泛型函数
- 在
C#
中,泛型函数通过在方法名称后使用尖括号<T>
形式的类型参数来定义,其中T
是类型参数的占位符。可以有多个类型参数,例如<T, U>
等。
- 在
public class Program |
- 多个类型参数
- 泛型函数可以有多个类型参数,使用逗号分隔。
public static void Swap<T, U>(ref T first, ref U second) |
在这个示例中,Swap
方法使用了两个类型参数T
和U
,可以交换不同类型的变量。
- 类型约束
- 可以对泛型类型参数施加约束,以限制可用的类型。例如,限制类型参数必须实现某个接口或继承自某个类。
public static void Display<T>(T value) where T : IComparable<T> |
在这个示例中,Display<T>
方法只能接受实现了IComparable<T>
接口的类型。
- 泛型函数的优点
- 代码重用: 泛型函数允许编写通用的代码,可以与不同的数据类型一起使用,而无需为每种类型编写重复的代码。
- 类型安全: 使用泛型可以在编译时检查类型一致性,避免运行时错误。
- 性能: 泛型函数不会像非泛型集合类那样引入装箱和拆箱操作(对于值类型),因此可以提高性能。
41. LINQ (语言集成查询)
LINQ
(语言集成查询)是C#
提供的一种功能,使得开发者可以使用类似SQL
的语法来查询和操作各种数据源,如对象集合、数据库、XML
等。LINQ
提供了一种统一的查询语法和机制,使得数据查询和操作更加简洁、类型安全和可维护。
LINQ
的基本概念- 统一的查询语法: 无论数据来源是什么,
LINQ
提供了统一的查询语法,简化了数据访问和操作的过程。 - 延迟执行:
LINQ
查询通常是延迟执行的,即只有在查询结果被真正访问时,才会执行查询。这可以提高性能,尤其是在处理大型数据集时。
- 统一的查询语法: 无论数据来源是什么,
LINQ
查询语法- 查询语法类似
SQL
,是一种声明式的语法,描述我们希望从数据源中提取什么样的数据。
- 查询语法类似
int[] numbers = { 5, 10, 8, 3, 6, 12 }; |
LINQ
方法语法- 方法语法使用标准查询运算符方法,这些方法是静态方法,可以像链条一样连接起来。
int[] numbers = { 5, 10, 8, 3, 6, 12 }; |
LINQ
的核心概念之一就是它可以作用于实现了IEnumerable<T>
接口的任何数据源。
using System; |
总而言之,LINQ
是C#
中的一个强大功能,它提供了一个统一的方式来查询和操作各种数据源。通过 LINQ
,开发者可以使用简单、清晰且类型安全的代码来执行复杂的数据操作。
附录
文件还未上传 Github