内容来自书籍《C# 10 in a Nutshell》
Author:Joseph Albahari
需要该电子书的小伙伴,可以留下邮箱,有空看到就会发送的
类的格式是:
在前面的是Attributes and class modifiers:public, internal,abstract, sealed, static, unsafe, and partial.
然后是类的名字
最后的是大括号包裹的类的成员:methods, properties, indexers, events, fields, constructors,overloaded operators, nested types, and a finalizer
字段是类的在其中一个成员,其实就是一个变量。它支持以下几种修饰符
readonly
这个修饰符是为了防止类实例化之后,字段被修改,它只能在构造函数中或者在字段中直接初始化
可以同时声明并初始化多个字段
static readonly int legs = 8, eyes = 2;
一个方法的签名必须是独一无二的(在当前类),函数的签名包括它的名字和参数列表,但不包括函数的返回值,函数允许的修饰符有:
函数体有两种写法:
第一种是普遍的写法,也就是函数签名后跟大括号
还有一种就是函数签名后跟=>
箭头,然后是表达式,而且只能是单行,这叫做expression-bodied method
可以在方法的内部,再声明一个本地方法,如果是正常的本地方法,它就好像是闭包一样,可以捕捉周围的本地变量,如果是在本地方法加上静态修饰符,那么它就没办法捕捉本地变量了,这可以防止某些本地方法错误捕捉周围的变量
字段的初始化是发生在构造函数之前的,字段初始化的顺序是编写代码的顺序
解构函数,相当于是构造函数的反函数,一个构造函数代表获取一个集合的数据并将它们赋值给字段,然后一个解构函数就代表将字段中的值赋值到一个集合的变量中
解构函数必须的名字必须是Deconstruct
,然后必须有一个或多个out
参数
public void Deconstruct (out float width, out float height) { width = Width; height = Height; }
然后在使用的时候,不需要特殊的标记,只需要赋值
var rect = new Rectangle (3, 4); (float width, float height) = rect; // Deconstruction var (width, height) = rect; // simply
需要注意的是,解构函数也可以编写为扩展函数,也就是说,如果需要对某一个类型解构,但是类型的作者没有提供,那我们可以自己编写一个扩展函数解构类型
对象初始化是发生在构造函数执行后,可以给任何公开的字段或属性进行赋值。
Bunny b1 = new Bunny { Name="Bo", LikesCarrots=true, LikesHumans=false }; Bunny b2 = new Bunny ("Bo") { LikesCarrots=true, LikesHumans=false }; // 编译器会生成下面类似的代码 Bunny temp1 = new Bunny(); temp1.Name = "Bo"; temp1.LikesCarrots = true; temp1.LikesHumans = false; Bunny b1 = temp1;
属性看起来像是字段。但是实际上,属性包含逻辑,像是方法一样。比如下面的代码
Stock msft = new Stock(); msft.CurrentPrice = 30; msft.CurrentPrice -= 3; Console.WriteLine (msft.CurrentPrice);
单纯从外面看,你不知道这是属性还是字段
属性的声明像一个字段,但是不同的是,属性有两个访问器get和set
public class Stock { decimal currentPrice; public decimal CurrentPrice { get { return currentPrice; } set { currentPrice = value; } } }
get访问器方法,必须返回和属性类型一样的值,而set方法接受一个和属性类型一样的参数value。其实相当于两个方法,这两个方法并不一定和上面的代码一样,要有一个相同名字的字段存储值,它同样可以返回计算值。
属性的可用修饰符
如果一个属性只有get访问器,那么这个属性就是只读属性
public decimal Worth { get => currentPrice * sharesOwned; set => sharesOwned = value / currentPrice; }
最通用的属性实现是简单读取和写入一个私有的字段。那么可以用automatic property声明来让编译器帮我们实现
public decimal CurrentPrice { get; set; }
可以在Automatic properties的基础上,给属性初始化一个值,只需要
public decimal CurrentPrice { get; set; } = 123;
public class Note { public int Pitch { get; init; } = 20; public int Duration { get; init; } = 100; }
init-only的行为就像是一个read-only的属性
索引器提供了一种自然的语法来访问类或者结构体的元素。它和属性很相似,但是它是通过索引参数来访问而不是属性名。比如说string就有一个索引器,它就像数组一样访问元素,不同的是它的索引参数可以是任意类型
string s = "hello"; Console.WriteLine (s[0]); // 'h' Console.WriteLine (s[3]); // 'l'
class Sentence { string[] words = "The quick brown fox".Split(); public string this [int wordNum] { get { return words [wordNum]; } set { words [wordNum] = value; } } }
而且索引参数还可以是多个
public string this [int arg1, string arg2] { get { ... } set { ... } }
只需要将索引器的参数标记为Index
或者Range
就可以了
静态的构造函数,每种类型只会执行一次,而不是每次实例化都会执行。而且必须是无参构造
静态字段的初始化发生在静态构造函数的之前。如果类没有静态构造函数,那么静态字段的初始化发生在这个类型被使用的时候
Console.WriteLine (Foo.X); // 3 class Foo { public static Foo Instance = new Foo(); public static int X = 3; Foo() => Console.WriteLine (X); // 0 }
静态类必须由静态成员组成,类不能被实例化或子类化
这个方法是唯一一个,在实例被垃圾回收器回收时所调用的方法
class Class1 { ~Class1() { ... } }
nameof()
操作符,会返回任何符号的名字的字符串
C#只能单继承,用:
来表示继承或者实现
public class Asset { public string Name; } public class Stock : Asset { public long SharesOwned; }
引用是多态的。这说明,一个变量x,可以引用一个对象y,y属于x的子类
一个引用对象可以:
在之前,如果向下转换时,失败了,会抛出转换错误的异常,但是现在可以使用关键字as
来转换,如果转换失败了,引用只会指向null,而不是抛出异常
Asset a = new Asset(); Stock s = a as Stock; // s is null; no exception thrown
is
操作符用来测试一个变量是否匹配某个模式。C#有几种模式,最重要的一种是type pattern类型模式。在这个场景下,is
操作符会测试一个引用的转换是不是成功
if (a is Stock) Console.WriteLine (((Stock)a).SharesOwned);
可以直接转换成某个类型的变量
if (a is Stock s) Console.WriteLine (s.SharesOwned);
标记为virtual
的函数,可以被子类重写。方法、属性、索引器和事件都可以声明为virtual
public class Asset { public string Name; public virtual decimal Liability => 0; } public class House : Asset { public decimal Mortgage; public override decimal Liability => Mortgage; }
返回值是返回协变体是允许的
public class Asset { public string Name; public virtual Asset Clone() => new Asset { Name = Name }; } public class House : Asset { public decimal Mortgage; public override House Clone() => new House { Name = Name, Mortgage = Mortgage }; }
子类的成员和基类的成员一样的时候,子类的成员会覆盖基类的成员
public class A{ public int Counter = 1; } public class B : A{ public int Counter = 2; }
但是编译器会有warning,所以最好的做法应该是用关键字new
public class B : A { public new int Counter = 2; }
那么new
重写和overrider
重写的区别是,new
是覆盖,当基类引用指向子类实例的时候,调用方法时会使用基类的方法,但是如果是overrider
重写了,那么就算是基类引用,调用的还是子类实例的方法
Overrider over = new Overrider(); BaseClass b1 = over; over.Foo(); // Overrider.Foo b1.Foo(); // Overrider.Foo Hider h = new Hider(); BaseClass b2 = h; h.Foo(); // Hider.Foo b2.Foo(); // BaseClass.Foo
一个overrider
的函数,可以用sealed
关键字阻止它的子类继续重写这个实现
这个关键字还可以用在类上,可以阻止派生子类
需要注意的是,new
覆盖是不可以用sealed
的
public class B { int x = 1; // 3 public B (int x) { ... // 4 } } public class D : B { int y = 1; // 1 public D (int x) : base (x + 1) // 2 { ... // 5 } }
装箱是一个值类型转换到一个引用类型,这个引用类型可以是一个object
类型或者一个接口
int x = 9; object obj = x; // Box the int
拆箱就是强转成值类型int y = (int)obj; // Unbox the int
C#可以在运行时获取实例的类型System.Type
,有两种方式获取:
GetType
typeof
操作符public class Object { public Object(); public extern Type GetType(); public virtual bool Equals (object obj); public static bool Equals (object objA, object objB); public static bool ReferenceEquals (object objA, object objB); public virtual int GetHashCode(); public virtual string ToString(); protected virtual void Finalize(); protected extern object MemberwiseClone(); }
结构体和类很相似,不同点在于
struct
是一个值类型,类是一个引用类型struct
不支持继承struct
是值类型,所以是没有null的,它的默认值是空的结构体readonly struct Point { public int X, Y; }
可以在结构体上标注只读,这样结构体所有字段都是只读的,也可以给方法标注只读,这样方法如果想去修改任何字段,都会报编译时错误
不像那些引用类型,都是存活在堆中的,结构体的内存是根据变量声明的位置决定的。如果一个值类型出现在参数或者本地变量中,那么它是存活在栈中的
但是如果值类型是出现在类的字段中,那它也是在堆中存活的
如果给结构体添加ref
关键字,那么这个结构体只能存活在栈中
internal
和protected
的结合可以暴露internal
的成员给friendassembly访问,通过使用attribute来实现
System.Runtime.CompilerServices.InternalsVisibleTo
[assembly: InternalsVisibleTo ("Friend")]
接口就像是无状态的类
接口可以继承其他接口
当两个接口具有相同的方法签名的时候,我们可以显式实现某一个接口的方法
interface I1 { void Foo(); } interface I2 { int Foo(); } public class Widget : I1, I2 { public void Foo() { Console.WriteLine ("Widget's implementation of I1.Foo"); } int I2.Foo() { Console.WriteLine ("Widget's implementation of I2.Foo"); return 42; } }
如果是这样,那么这个类中实现了两个接口的方法,如果需要调用某个接口的方法,只能通过cast
来做到
Widget w = new Widget(); w.Foo(); // Widget's implementation of I1.Foo ((I1)w).Foo(); // Widget's implementation of I1.Foo ((I2)w).Foo(); // Widget's implementation of I2.Foo
一个接口的成员实现,默认是seal
密封的,如果需要作为基类需要给子类实现,需要添加关键字virtual
public interface IUndoable { void Undo(); } public class TextBox : IUndoable { public virtual void Undo() => Console.WriteLine ("TextBox.Undo"); } public class RichTextBox : TextBox { public override void Undo() => Console.WriteLine ("RichTextBox.Undo"); }
public interface IUndoable { void Undo(); } public class TextBox : IUndoable { void IUndoable.Undo() => Console.WriteLine ("TextBox.Undo"); } public class RichTextBox : TextBox, IUndoable { public void Undo() => Console.WriteLine ("RichTextBox.Undo"); }
一个子类可以重新实现任何基类已经实现的接口方法。上面的例子是,基类实现的方法不是virtual
的,所以子类不能覆盖,子类必须实现接口并重新实现
C# 8增加了,默认方法作为接口的成员
枚举是一个特殊的类型,它是一组被命名的数字常量。
public enum BorderSide { Left, Right, Top, Bottom }
默认情况下,所有成员都是int类型,从0开始分配
也可以选择一个指定的数字类型
public enum BorderSide : byte { Left, Right, Top, Bottom }
也可以显式指定一个值给每个成员
public enum BorderSide : byte { Left=1, Right=2, Top=10, Bottom=11 }
枚举可以转换成一个数字类型,反向操作也是可以的。
int i = (int) BorderSide.Left; BorderSide side = (BorderSide) i; bool leftOrRight = (int) side <= 2;
也可以将枚举的成员结合在一起,防止歧义,成员的结合需要显式赋值
[Flags] enum BorderSides { None=0, Left=1, Right=2, Top=4, Bottom=8 }
可以对这个枚举使用位操作符,比如|
和&
BorderSides leftRight = BorderSides.Left | BorderSides.Right; if ((leftRight & BorderSides.Left) != 0) Console.WriteLine ("Includes Left"); // Includes Left string formatted = leftRight.ToString();// "Left, Right" BorderSides s = BorderSides.Left; s |= BorderSides.Right; Console.WriteLine (s == leftRight);// True s ^= BorderSides.Right; // Toggles BorderSides.Right Console.WriteLine (s); // Left
而且这种枚举可以递归定义
[Flags] enum BorderSides { None=0, Left=1, Right=1<<1, Top=1<<2, Bottom=1<<3, LeftRight = Left | Right, TopBottom = Top | Bottom, All = LeftRight | TopBottom }
枚举的真实值是数字,那么在转换的时候,可以用一个没有定义的值转换,不会发生错误,可以使用方法
Enum.IsDefined
进行验证
BorderSide side = (BorderSide) 12345; Console.WriteLine (Enum.IsDefined (typeof (BorderSide), side)); // False
C#有两种机制,来使得不同的类型重用代码:继承和范型
继承表达的是重用一个基础类型,范型表达的是重用一个模板包含着一个占位符类型。范型和继承比较,它增加了类型安全和减少强转和装箱
范型类型声明一个类型参数,它是一个占位符,会在使用的地方填上具体的类型。
public class Stack<T> { int position; T[] data = new T[100]; public void Push (T obj)=> data[position++] = obj; public T Pop()=> data[--position]; } var stack = new Stack<int>(); stack.Push (5); stack.Push (10);
Stack<int>
有如下定义,类名称会是一个hashed值避免混乱
public class ### { int position; int[] data = new int[100]; public void Push (int obj)=> data[position++] = obj; public int Pop()=> data[--position]; }
技术上,将Stack<T>
叫做开放类型,Stack<int>
叫做封闭类型。在运行时,所有的范型类型实例都是封闭的
运行时是不存在开放范型类型的。它们在编译时就变成封闭类型了。然而,还有一种可能是,运行时存在不受约束的范型类型,纯粹作为Type
对象,唯一的方法是使用typeof
操作符
class A<T> {} class A<T1,T2> {} Type a1 = typeof (A<>); // Unbound type Type a2 = typeof (A<,>); // Use commas to indicate multiple type args. Type a3 = typeof (A<int,int>); class B<T> { void X() { Type t = typeof (T); } }
可以用default
关键字区获取范型类型参数的默认值,如果是引用类型的默认值是null,如果是值类型的默认值就是按位置零
范型约束,约束可以应用在类型参数中,具有更多指定的类型参数
where T : base-class // 基类约束 where T : interface // 接口约束 where T : class // 引用类型约束 where T : class? // 可空引用类型约束 where T : struct // 值类型约束 where T : new() // 无参构造约束 where U : T // 裸类型约束 where T : notnull // 不可空值类型或者不可空引用类型
假如A可以转换成B,那么X有一个协变类型参数,比如X<A>
可以转换为X<B>
需要注意的是,可以转换的意思是,A是B的子类或者实现,然后A可以隐式转换成B。数字转换、装箱转换和自定义转换都不包括在内
协变不是自动的
class Animal {} class Bear : Animal {} class Camel : Animal {} Stack<Bear> bears = new Stack<Bear>(); Stack<Animal> animals = bears; // Compile-time error class ZooCleaner { public static void Wash<T> (Stack<T> animals) where T : Animal { ... } } Stack<Bear> bears = new Stack<Bear>(); ZooCleaner.Wash (bears);
可以在接口或者委托上声明一个协变类型参数,通过将它标记为out
public interface IPoppable<out T> { T Pop(); }
这个out
修饰符标记的意思是T只是被用来作为输出的参数
var bears = new Stack<Bear>(); bears.Push (new Bear()); // Bears implements IPoppable<Bear>. We can convert to IPoppable<Animal>: IPoppable<Animal> animals = bears; // Legal Animal a = animals.Pop();
可以这样转换,是因为在限制的前提下,这种转换是类型安全的,因为编译器是阻止调用将T作为输入参数的方法,out
修饰符保证了这个类型参数只会出现在输出参数中,而不会输入
逆变,是协变的逆转。比如说A可以隐式转换为B,然后X<A>
可以协变为X<B>
,如果是逆变,那就是X<B>
转换为X<A>
。这个可以用在,当类型参数T只会出现在输入参数中,可以用in
修饰符,这样我们就可以做到
IPushable<Animal> animals = new Stack<Animal>(); IPushable<Bear> bears = animals; // Legal bears.Push (new Bear());
C#的范型和C++的模板非常相似,但是内部运行的机制是非常不一样的。两个都是必须是声明的和实际使用的结合在一起,声明的占位符必须让使用者填上。然而,C#的范型是可以编译成库的,因为声明和使用的结合成封闭类型是一直到运行时才会发生。但是C++模板的话,是发生在编译时的。这意味着C++不能发布模板库,它们只会存在源码中。