一、特点
- 面向对象(封装、继承、多态)、面向组件
- 类型安全、支持null对象
- 支持Lambda表达式(函数式编程)
- 支持GC
- LINQ(语言集成查询)
- 异步
- 统一类型系统(object)
- 面向.NET
- 没有全局函数
- 提供delegates来模拟指针的功能
注:.NET是名为公共语言运行时(CLR)的虚执行系统和一组类库
二、Hello World
using System; //使用System命名空间的using指令
class Hello
{
static void Main() //C#程序的入口
{
Console.WriteLine("Hello, World");
}
}
三、类型 — 类型和变量
- 值类型:变量直接包含数据。每个变量都有自己的副本。
- 简单类型
a. 有符号整型:sbyte、short、int、long
b. 无符号整型:byte、ushort、uint、ulong
c. Unicode 字符:char(UTF-16)
d. IEEE 二进制浮点:float、double
e. 高精度十进制浮点数:decimal
f. 布尔值:bool(true 或 false) - 枚举类型:enum E {…} 格式的用户定义类型。 enum 类型是一种包含已命名常量的独特类型。 每个 enum 类型都有一个基础类型(必须是八种整型类型之一)。 enum 类型的值集与基础类型的值集相同。
- 结构类型:格式为 struct S {…} 的用户定义类型
- 可以为 null 的值类型:值为 null 的其他所有值类型的扩展
- 元组值类型:格式为 (T1, T2, …) 的用户定义类型|
- 引用类型:变量存储对数据(对象)的引用。两个变量可以引用同一个对象,对任一变量执行运算可能会导致另一个变量引用的对象。
- 类类型
a. 其他所有类型的最终基类:object
b. Unicode 字符串:string(UTF-16)
c. 格式为 class C {…} 的用户定义类型 - 接口类型:格式为 interface I {…} 的用户定义类型
- 数组类型:一维、多维和交错。 例如:int[]、int[,] 和 int[][]
- 委托类型:格式为 delegate int D(…) 的用户定义类型
- 装箱拆箱
int i = 123;
object o = i; // Boxing:基础数据类型转object
int j = (int)o; // Unboxing
四、类型 — 程序结构
程序、命名空间、类型、成员、程序集
五、类型 — 类和对象
public class Point
{
public int X { get; }
public int Y { get; }
public Point(int x, int y) => (X, Y) = (x, y);
}
var p1 = new Point(0, 0); //new:为新实例分配内存,调用构造函数来初始化实例,并返回对实例的引用
var p2 = new Point(10, 20);
六、类型 — 类型参数
类型参数是用尖括号括起来的类型参数名称列表
public class Pair<TFirst, TSecond>
{
public TFirst First { get; }
public TSecond Second { get; }
public Pair(TFirst first, TSecond second) =>
(First, Second) = (first, second);
}
var pair = new Pair<int, string>(1, "two");
int i = pair.First; // TFirst int
string s = pair.Second; // TSecond string
包含类型自变量的泛型类型(如上面的 Pair<int,string>)被称为 构造泛型类型。
七、类型 — 基类(单继承)
public class Point3D : Point
{
public int Z { get; set; }
public Point3D(int x, int y, int z) : base(x, y)
{
Z = z;
}
}
Point a = new Point(10, 20);
Point b = new Point3D(10, 20, 30);
类继承其基类的成员。 继承意味着一个类隐式包含其基类的几乎所有成员。 类不继承实例、静态构造函数以及终结器。 派生类可以在其继承的成员中添加新成员,但无法删除继承成员的定义。
可以将类类型隐式转换成其任意基类类型。 类类型的变量可以引用相应类的实例或任意派生类的实例。 例如,类声明如上,Point 类型的变量可以引用 Point 或 Point3D。
八、类型 — 结构
主要目的:存储数据值。 结构不能声明基类型,从 System.ValueType 隐式派生,不能从 struct 类型派生其他 struct 类型,这些类型已隐式密封。
public struct Point
{
public double X { get; }
public double Y { get; }
public Point(double x, double y) => (X, Y) = (x, y);
}
九、类型 — 接口(多实现)
接口定义了可由类和结构实现的协定。 接口可以包含方法、属性、事件和索引器。 接口通常不提供所定义成员的实现,仅指定必须由实现接口的类或结构提供的成员。
interface IControl
{
void Paint();
}
interface ITextBox : IControl
{
void SetText(string text);
}
interface IListBox : IControl
{
void SetItems(string[] items);
}
interface IComboBox : ITextBox, IListBox { }
多实现。当类或结构实现特定接口时,此类或结构的实例可以隐式转换成相应的接口类型。
interface IDataBound
{
void Bind(Binder b);
}
public class EditBox : IControl, IDataBound
{
public void Paint() { }
public void Bind(Binder b) { }
}
EditBox editBox = new EditBox();
IControl control = editBox;
IDataBound dataBound = editBox;
十、类型 — 枚举
枚举类型定义了一组常数值。
public enum SomeRootVegetable
{
HorseRadish,
Radish,
Turnip
}
[Flags]
public enum Seasons
{
None = 0,
Summer = 1,
Autumn = 2,
Winter = 4,
Spring = 8,
All = Summer | Autumn | Winter | Spring
}
var turnip = SomeRootVegetable.Turnip;
var spring = Seasons.Spring;
var startingOnEquinox = Seasons.Spring | Seasons.Autumn;
var theYear = Seasons.All;
十一、类型 — 可为null的类型
可为 null 的值类型(结构或枚举)由 System.Nullable 表示。
十二、类型 — 元组
int? optionalInt = default; //null
optionalInt = 5; //5
string? optionalText = default; //null
optionalText = "Hello World."; //Hello World.
十三、程序构建 — 成员
class 的成员要么是静态成员,要么是实例成员。 静态成员属于类,而实例成员则属于对象(类实例)。
- 常量:与类相关联的常量值
- 字段:与类关联的变量
- 方法:类可执行的操作
- 属性:与读取和写入类的已命名属性相关联的操作
- 索引器:与将类实例编入索引(像处理数组一样)相关联的操作
- 事件:类可以生成的通知
- 运算符:类支持的转换和表达式运算符
- 构造函数:初始化类实例或类本身所需的操作
- 终结器:永久放弃类实例之前执行的操作
- 类型:类声明的嵌套类型
十四、程序构建 — 辅助功能
- public:访问不受限制。
- private:访问仅限于此类。
- protected:访问仅限于此类或派生自此类的类。
- internal:仅可访问当前程序集(.exe 或 .dll)。
- protected internal:仅可访问此类、从此类中派生的类,或者同一程序集中的类。
- private protected:仅可访问此类或同一程序集中从此类中派生的类。
十五、程序构建 — 字段
字段是与类或类实例相关联的变量。
- 静态字段:使用静态修饰符声明的字段定义。 静态字段只指明一个存储位置。 无论创建多少个类实例,永远只有一个静态字段副本。
- 实例字段:不使用静态修饰符声明的字段定义。 每个类实例均包含相应类的所有实例字段的单独副本。
public class Color
{
//静态字段
public static readonly Color Black = new Color(0, 0, 0);
public static readonly Color White = new Color(255, 255, 255);
public static readonly Color Red = new Color(255, 0, 0);
public static readonly Color Green = new Color(0, 255, 0);
public static readonly Color Blue = new Color(0, 0, 255);
//实例字段
public byte R;
public byte G;
public byte B;
public Color(byte r, byte g, byte b)
{
R = r;
G = g;
B = b;
}
}
注:可以使用 readonly 修饰符声明只读字段。只能在字段声明期间或在同一个类的构造函数中向只读字段赋值。
十六、程序构建 — 方法
静态方法 是通过类进行访问。 实例方法 是通过类实例进行访问。
当方法主体是单个表达式时,可使用紧凑表达式格式定义方法。
public override string ToString() => "This is an object";
参数
- 值参数:对应局部变量。
- 引用参数:ref修饰符进行声明,需要带有明确值。
- 输出参数:out修饰符进行声明,与引用参数类似,不同在于不要求向调用方提供的自变量显式赋值。
- 参数数组:params修饰符进行声明,允许向方法传递数量不定的自变量,只能是方法的最后一个参数,类型必须是一维数组。
//引用参数
static void Swap(ref int x, ref int y)
{
int temp = x;
x = y;
y = temp;
}
public static void SwapExample()
{
int i = 1, j = 2;
Swap(ref i, ref j);
Console.WriteLine($"{i} {j}"); // "2 1"
}
//输入参数
static void Divide(int x, int y, out int result, out int remainder)
{
result = x / y;
remainder = x % y;
}
public static void OutUsage()
{
Divide(10, 3, out int res, out int rem);
Console.WriteLine($"{res} {rem}"); // "3 1"
}
//参数数组
public class Console
{
public static void Write(string fmt, params object[] args) { }
public static void WriteLine(string fmt, params object[] args) { }
// ...
}
方法主体和局部变量
C# 要求必须先 明确赋值 局部变量,然后才能获取其值。
静态和实例方法
- static修饰的是静态方法,静态方法不对特定的实例起作用,只能直接访问静态成员。
- 没有用static修饰的是实例方法,对特定的实例起作用,并能够访问静态和实例成员。 其中调用实例方法的实例可以作为 this 显式访问。 在静态方法中引用 this 会生成错误。
虚方法、重写方法和抽象方法
- 虚方法:virtual修饰
- 重写方法:可以在派生类中重写虚方法
- 抽象方法:abstract修饰,没有代码实现的虚方法
方法重载
同一类中可以有多个同名方法,具有唯一签名。编译器使用重载决策来调用特定方法,找到匹配度最高的方法,若找不到则报错。
十七、程序构建 — 其他函数成员
构造函数
- 实例构造函数:实现初始化类实例所需执行的操作的成员。可重载并且可具有可选参数。不会被继承, 类中只能包含实际上已在该类中声明的实例构造函数。 如果没有为类提供实例构造函数,则会自动提供不含参数的空实例构造函数。
- 静态构造函数:实现在首次加载类时初始化类本身所需执行的操作的成员,不能被继承
- 声明方式:没有返回类型,且与所含类同名。 如果构造函数声明包含 static 修饰符,则声明的是静态构造函数。 否则,声明的是实例构造函数。
MyList<string> list1 = new MyList<string>();
MyList<string> list2 = new MyList<string>(10);
“属性”
- 是字段的自然扩展,不指明存储位置,包含访问器,用于指定在读取或写入属性值时执行的语句。
- 属性的声明方式与字段相似,区别是属性声明以在分隔符 { 和 } 之间写入的 get 访问器或 set 访问器结束,而不是以分号结束。 同时具有 get 访问器和 set 访问器的属性是“读写属性”。 只有 get 访问器的属性是“只读属性”。 只有 set 访问器的属性是“只写属性”。
- get 访问器对应于包含属性类型的返回值的无参数方法。 set 访问器对应于包含一个名为 value 的参数但不含返回类型的方法。 get 访问器会计算属性的值。 set 访问器会为属性提供新值。 当属性是赋值的目标,或者是 ++ 或 – 的操作数时,会调用 set 访问器。 在引用了属性的其他情况下,会调用 get 访问器。
- 支持实例属性和静态属性。 静态属性使用静态修饰符进行声明,而实例属性则不使用静态修饰符进行声明。
- 属性的访问器可以是虚的。 如果属性声明包含 virtual、abstract 或 override 修饰符,则适用于属性的访问器。
MyList<string> names = new MyList<string>();
names.Capacity = 100; // Invokes set accessor
int i = names.Count; // Invokes get accessor
int j = names.Capacity; // Invokes get accessor
索引器
- 借助 索引器 成员,可以将对象编入索引(像处理数组一样)。 索引器的声明方式与属性类似,不同之处在于,索引器成员名称格式为 this 后跟在分隔符 [ 和 ] 内写入的参数列表。 这些参数在索引器的访问器中可用。 类似于属性,索引器分为读写、只读和只写索引器,且索引器的访问器可以是虚的。
- 索引器可被重载。 一个类可声明多个索引器,只要其参数的数量或类型不同即可。
MyList<string> names = new MyList<string>();
names.Add("Liz");
names.Add("Martha");
names.Add("Beth");
for (int i = 0; i < names.Count; i++)
{
string s = names[i];
names[i] = s.ToUpper();
}
事件
- 借助 事件 成员,类或对象可以提供通知。 事件的声明方式与字段类似,区别是事件声明包括 event 关键字,且类型必须是委托类型。
- 在声明事件成员的类中,事件的行为与委托类型的字段完全相同(前提是事件不是抽象的,且不声明访问器)。 字段存储对委托的引用,委托表示已添加到事件的事件处理程序。 如果没有任何事件处理程序,则字段为 null。
- 对于需要控制事件的基础存储的高级方案,事件声明可以显式提供 add 和 remove 访问器,这与属性的 set 访问器类似。
class EventExample
{
static int s_changeCount;
static void ListChanged(object sender, EventArgs e)
{
s_changeCount++;
}
public static void Usage()
{
var names = new MyList<string>();
names.Changed += new EventHandler(ListChanged);
names.Add("Liz");
names.Add("Martha");
names.Add("Beth");
Console.WriteLine(s_changeCount); // "3"
}
}
运算符
- 运算符 是定义向类实例应用特定表达式运算符的含义的成员。 可以定义三种类型的运算符:一元运算符、二元运算符和转换运算符。 所有运算符都必须声明为 public 和 static。
- MyList 类会声明两个运算符:operator == 和 operator !=。 对于向 MyList 实例应用这些运算符的表达式来说,这些重写的运算符向它们赋予了新的含义。 具体而言,这些运算符定义的是两个 MyList 实例的相等性(使用其 Equals 方法比较所包含的每个对象)。
MyList<int> a = new MyList<int>();
a.Add(1);
a.Add(2);
MyList<int> b = new MyList<int>();
b.Add(1);
b.Add(2);
Console.WriteLine(a == b); // Outputs "True" 两个列表包含的对象不仅数量相同,而且值和顺序也相同
b.Add(3);
Console.WriteLine(a == b); // Outputs "False" a 和 b 引用不同的 MyList<int> 实例
终结器
- 终结器 是实现完成类实例所需的操作的成员。 通常,需要使用终结器来释放非托管资源。 终结器既不能包含参数和可访问性修饰符,也不能进行显式调用。 实例的终结器在垃圾回收期间自动调用
- 垃圾回收器在决定何时收集对象和运行终结器时有很大自由度。 具体而言,终结器的调用时间具有不确定性,可以在任意线程上执行终结器。 因为这样或那样的原因,只有在没有其他可行的解决方案时,类才能实现终结器。
- 处理对象析构的更好方法是使用 using 语句。
十八、程序构建 — 表达式
- 在 操作数 和 运算符 的基础之上构造而成。表达式的运算符指明了向操作数应用的运算。 运算符的示例包括 +、-、*、/ 和 new。 操作数的示例包括文本、字段、局部变量和表达式。
- 具有优先级。若优先级相同,运算符的 结合性 决定了运算的执行顺序:
1)除了赋值运算符和 null 合并运算符之外,所有二元运算符均为左结合运算符,即从左向右执行运算。 例如,x + y + z 将计算为 (x + y) + z。
2)赋值运算符、null 合并 ?? 和 ??= 运算符和条件运算符 ?: 为右结合运算符,即从右向左执行运算。 例如,x = y = z 将计算为 x = (y = z)。 - 可以使用括号控制优先级和结合性。
- 大部分运算符可重载。 借助运算符重载,可以为一个或两个操作数为用户定义类或结构类型的运算指定用户定义运算符实现代码。
十九、程序构建 — 语句
程序操作使用语句进行表示。
- 使用代码块,可以在允许编写一个语句的上下文中编写多个语句。 代码块是由一系列在分隔符 { 和 } 内编写的语句组成。
- 声明语句 用于声明局部变量和常量。
- 表达式语句 用于计算表达式。 可用作语句的表达式包括方法调用、使用 new 运算符的对象分配、使用 = 和复合赋值运算符的赋值、使用 ++ 和 – 运算符和 await 表达式的递增和递减运算。
- 选择语句 用于根据一些表达式的值从多个可能的语句中选择一个以供执行。 此组包含 if 和 switch 语句。
- 迭代语句 用于重复执行嵌入语句。 此组包含 while、do、for 和 foreach 语句。
- 跳转语句 用于转移控制权。 此组包含 break、continue、goto、throw、return 和 yield 语句。
- try…catch 语句用于捕获在代码块执行期间发生的异常,try…finally 语句用于指定始终执行的最终代码,无论异常发生与否。
- checked 和 unchecked 语句用于控制整型类型算术运算和转换的溢出检查上下文。
- lock 语句用于获取给定对象的相互排斥锁定,执行语句,然后解除锁定。
- using 语句用于获取资源,执行语句,然后释放资源。
下面列出了可使用的语句类型:
- 局部变量声明。
- 局部常量声明。
- 表达式语句。
- if 语句。
- switch 语句。
- while 语句。
- do 语句。
- for 语句。
- foreach 语句。
- break 语句。
- continue 语句。
- goto 语句。
- return 语句。
- yield 语句。
- throw 和 try 语句。
- checked 和 unchecked 语句。
- lock 语句。
- using 语句。
二十、主要语言区域 — 数组、集合和 LINQ
- 数组是一种数据结构*_,包含许多通过计算索引访问的变量。 数组中的变量(亦称为数组的“元素”)均为同一种类型。 将这种类型称为数组的“元素类型”。
- 数组类型是引用类型,声明数组变量只是为引用数组实例预留空间。 实际的数组实例是在运行时使用 new 运算符动态创建而成。 new 运算指定了新数组实例的长度,然后在此实例的生存期内固定使用这个长度。 数组元素的索引介于 0 到 Length - 1 之间。 new 运算符自动将数组元素初始化为其默认值(例如,所有数值类型的默认值为 0,所有引用类型的默认值为 null)。
- 支持多维数组。 数组类型的维数(亦称为数组类型的秩)是 1 与数组类型方括号内的逗号数量相加的结果。
int[] a1 = new int[10];
int[,] a2 = new int[10, 5];
int[,,] a3 = new int[10, 5, 2];
二十一、主要语言区域 — 字符串内插
- 能够通过定义表达式(其结果放置在格式字符串中)来设置字符串格式。
- 内插字符串通过 $ 标记来声明。 字符串插内插计算 { 和 } 之间的表达式,然后将结果转换为 string,并将括号内的文本替换为表达式的字符串结果。
Console.WriteLine($"The low and high temperature on {weatherData.Date:MM-DD-YYYY}");
Console.WriteLine($" was {weatherData.LowTemp} and {weatherData.HighTemp}.");
// Output (similar to):
// The low and high temperature on 08-11-2020
// was 5 and 30.
二十二、主要语言区域 — 模式匹配
查询对象的状态并基于该状态执行代码。 你可以检查属性和字段的类型和值,以确定要执行的操作。 switch 表达式是模式匹配的主要表达式。
二十三、主要语言区域 — 委托和 Lambda 表达式
- 委托类型delegate 表示对具有特定参数列表和返回类型的方法的引用。 通过委托,可以将方法视为可分配给变量并可作为参数传递的实体。 委托还类似于其他一些语言中存在的“函数指针”概念。 与函数指针不同,委托是面向对象且类型安全的。
- 委托可以引用静态方法或实例方法。 引用实例方法的委托还会引用特定对象,通过委托调用实例方法时,该对象会变成调用中的 this。
- 可以使用匿名函数创建委托,这些函数是在声明时创建的“内联方法”。 匿名函数可以查看周围方法的局部变量。
- 委托不知道或不在意其所引用的方法的类。 重要的是,引用的方法具有与委托相同的参数和返回类型。
delegate double Function(double x);
class Multiplier
{
double _factor;
public Multiplier(double factor) => _factor = factor;
public double Multiply(double x) => x * _factor;
}
class DelegateExample
{
static double[] Apply(double[] a, Function f)
{
var result = new double[a.Length];
for (int i = 0; i < a.Length; i++) result[i] = f(a[i]);
return result;
}
public static void Main()
{
double[] a = { 0.0, 0.5, 1.0 };
double[] squares = Apply(a, (x) => x * x); //静态方法
double[] sines = Apply(a, Math.Sin); //静态方法
Multiplier m = new Multiplier(2.0); //实例方法
double[] doubles = Apply(a, m.Multiply);
//匿名函数
double[] doubles = Apply(a, (double x) => x * 2.0);
}
}
二十四、主要语言区域 — async/await
异步的两个关键字,将 async 修饰符添加到方法声明中,以声明这是异步方法。 await 运算符通知编译器异步等待结果完成。 控件返回给调用方,该方法返回一个管理异步工作状态的结构。 结构通常是 System.Threading.Tasks.Task,但可以是任何支持 awaiter 模式的类型。 这些功能使你能够编写这样的代码:以其同步对应项的形式读取,但以异步方式执行。
//下载 Microsoft Docs 的主页
public async Task<int> RetrieveDocsHomePage()
{
var client = new HttpClient();
byte[] content = await client.GetByteArrayAsync("https://docs.microsoft.com/");
Console.WriteLine($"{nameof(RetrieveDocsHomePage)}: Finished downloading.");
return content.Length;
}
- 方法声明包含 async 修饰符。
- 方法 await 的主体是 GetByteArrayAsync 方法的返回。
- return 语句中指定的类型与方法的 Task 声明中的类型参数匹配。 (返回 Task 的方法将使用不带任何参数的 return 语句)。
二十五、主要语言区域 — 属性
程序通过定义和使用特性来指定此类额外的声明性信息。
以下示例声明了 HelpAttribute 特性,可将其附加到程序实体,以提供指向关联文档的链接。
public class HelpAttribute : Attribute
{
string _url;
string _topic;
public HelpAttribute(string url) => _url = url;
public string Url => _url;
public string Topic
{
get => _topic;
set => _topic = value;
}
}
所有特性类都派生自 .NET 库提供的 Attribute 基类。 特性的应用方式为,在相关声明前的方括号内指定特性的名称以及任意自变量。 如果特性的名称以 Attribute 结尾,那么可以在引用特性时省略这部分名称。
[Help("https://docs.microsoft.com/dotnet/csharp/tour-of-csharp/features")]
public class Widget
{
[Help("https://docs.microsoft.com/dotnet/csharp/tour-of-csharp/features",
Topic = "Display")]
public void Display(string text) { }
}
此示例将 HelpAttribute 附加到 Widget 类。 还向此类中的 Display 方法附加了另一个 HelpAttribute。 特性类的公共构造函数控制了将特性附加到程序实体时必须提供的信息。 可以通过引用特性类的公共读写属性(如上面示例对 Topic 属性的引用),提供其他信息。
可以在运行时使用反射来读取和操纵特性定义的元数据。 如果使用这种方法请求获取特定特性,便会调用特性类的构造函数(在程序源中提供信息)。 返回生成的特性实例。 如果是通过属性提供其他信息,那么在特性实例返回前,这些属性会设置为给定值。
Type widgetType = typeof(Widget);
object[] widgetClassAttributes = widgetType.GetCustomAttributes(typeof(HelpAttribute), false);
if (widgetClassAttributes.Length > 0)
{
HelpAttribute attr = (HelpAttribute)widgetClassAttributes[0];
Console.WriteLine($"Widget class help URL : {attr.Url} - Related topic : {attr.Topic}");
}
System.Reflection.MethodInfo displayMethod = widgetType.GetMethod(nameof(Widget.Display));
object[] displayMethodAttributes = displayMethod.GetCustomAttributes(typeof(HelpAttribute), false);
if (displayMethodAttributes.Length > 0)
{
HelpAttribute attr = (HelpAttribute)displayMethodAttributes[0];
Console.WriteLine($"Display method help URL : {attr.Url} - Related topic : {attr.Topic}");
}