Chaptor 3 . Creating Types in C#
内容来自书籍《C# 10 in a Nutshell》
Author:Joseph Albahari
需要该电子书的小伙伴,可以留下邮箱,有空看到就会发送的
Classes
类的格式是:
-
在前面的是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
Fields
字段是类的在其中一个成员,其实就是一个变量。它支持以下几种修饰符
The readonly modifier
readonly
这个修饰符是为了防止类实例化之后,字段被修改,它只能在构造函数中或者在字段中直接初始化
Declaring multiple fields together
可以同时声明并初始化多个字段
static readonly int legs = 8,
eyes = 2;
Methods
一个方法的签名必须是独一无二的(在当前类),函数的签名包括它的名字和参数列表,但不包括函数的返回值,函数允许的修饰符有:
Expression-bodied methods
函数体有两种写法:
第一种是普遍的写法,也就是函数签名后跟大括号
还有一种就是函数签名后跟=>
箭头,然后是表达式,而且只能是单行,这叫做expression-bodied method
Static Local methods
可以在方法的内部,再声明一个本地方法,如果是正常的本地方法,它就好像是闭包一样,可以捕捉周围的本地变量,如果是在本地方法加上静态修饰符,那么它就没办法捕捉本地变量了,这可以防止某些本地方法错误捕捉周围的变量
Instance Constructors
Constructor and field initialization order
字段的初始化是发生在构造函数之前的,字段初始化的顺序是编写代码的顺序
Deconstructors
解构函数,相当于是构造函数的反函数,一个构造函数代表获取一个集合的数据并将它们赋值给字段,然后一个解构函数就代表将字段中的值赋值到一个集合的变量中
解构函数必须的名字必须是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
需要注意的是,解构函数也可以编写为扩展函数,也就是说,如果需要对某一个类型解构,但是类型的作者没有提供,那我们可以自己编写一个扩展函数解构类型
Object Initializers
对象初始化是发生在构造函数执行后,可以给任何公开的字段或属性进行赋值。
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;
Properties
属性看起来像是字段。但是实际上,属性包含逻辑,像是方法一样。比如下面的代码
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。其实相当于两个方法,这两个方法并不一定和上面的代码一样,要有一个相同名字的字段存储值,它同样可以返回计算值。
属性的可用修饰符
Read-only and calculated properties
如果一个属性只有get访问器,那么这个属性就是只读属性
Expression-bodied properties
public decimal Worth
{
get => currentPrice * sharesOwned;
set => sharesOwned = value / currentPrice;
}
Automatic properties
最通用的属性实现是简单读取和写入一个私有的字段。那么可以用automatic property声明来让编译器帮我们实现
public decimal CurrentPrice { get; set; }
Property initializers
可以在Automatic properties的基础上,给属性初始化一个值,只需要
public decimal CurrentPrice { get; set; } = 123;
Init-only setters
public class Note
{
public int Pitch { get; init; } = 20;
public int Duration { get; init; } = 100;
}
init-only的行为就像是一个read-only的属性
Indexers
索引器提供了一种自然的语法来访问类或者结构体的元素。它和属性很相似,但是它是通过索引参数来访问而不是属性名。比如说string就有一个索引器,它就像数组一样访问元素,不同的是它的索引参数可以是任意类型
string s = "hello";
Console.WriteLine (s[0]); // 'h'
Console.WriteLine (s[3]); // 'l'
Implementing an indexer
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 { ... }
}
Using indices and ranges with indexers
只需要将索引器的参数标记为Index
或者Range
就可以了
Static Constructors
静态的构造函数,每种类型只会执行一次,而不是每次实例化都会执行。而且必须是无参构造
Static constructors and field initialization order
静态字段的初始化发生在静态构造函数的之前。如果类没有静态构造函数,那么静态字段的初始化发生在这个类型被使用的时候
Console.WriteLine (Foo.X); // 3
class Foo
{
public static Foo Instance = new Foo();
public static int X = 3;
Foo() => Console.WriteLine (X); // 0
}
Static Classes
静态类必须由静态成员组成,类不能被实例化或子类化
Finalizers
这个方法是唯一一个,在实例被垃圾回收器回收时所调用的方法
class Class1
{
~Class1()
{
...
}
}
The nameof operator
nameof()
操作符,会返回任何符号的名字的字符串
Inheritance
C#只能单继承,用:
来表示继承或者实现
public class Asset
{
public string Name;
}
public class Stock : Asset
{
public long SharesOwned;
}
Polymorphism
引用是多态的。这说明,一个变量x,可以引用一个对象y,y属于x的子类
Casting and Reference Conversions
一个引用对象可以:
- 隐式地向上转换到基类的引用
- 显式地向下转换到子类的引用
The as operator
在之前,如果向下转换时,失败了,会抛出转换错误的异常,但是现在可以使用关键字as
来转换,如果转换失败了,引用只会指向null,而不是抛出异常
Asset a = new Asset();
Stock s = a as Stock; // s is null; no exception thrown
The is operator
is
操作符用来测试一个变量是否匹配某个模式。C#有几种模式,最重要的一种是type pattern类型模式。在这个场景下,is
操作符会测试一个引用的转换是不是成功
if (a is Stock)
Console.WriteLine (((Stock)a).SharesOwned);
Introducing a pattern variable
可以直接转换成某个类型的变量
if (a is Stock s)
Console.WriteLine (s.SharesOwned);
Virtual Function Members
标记为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;
}
Covariant return types
返回值是返回协变体是允许的
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 };
}
Hiding Inherited Members
子类的成员和基类的成员一样的时候,子类的成员会覆盖基类的成员
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
Sealing Functions and Classes
一个overrider
的函数,可以用sealed
关键字阻止它的子类继续重写这个实现
这个关键字还可以用在类上,可以阻止派生子类
需要注意的是,new
覆盖是不可以用sealed
的
Constructors and Inheritance
Constructor and field initialization order
- 从子类到基类
- 字段初始化
- 调用基类构造函数
- 从基类到子类
- 构造函数体执行
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
}
}
Boxing and Unboxing
装箱是一个值类型转换到一个引用类型,这个引用类型可以是一个object
类型或者一个接口
int x = 9;
object obj = x; // Box the int
拆箱就是强转成值类型int y = (int)obj; // Unbox the int
The GetType Method and typeof Operator
C#可以在运行时获取实例的类型System.Type
,有两种方式获取:
- 从实例调用方法
GetType
- 对类型名称使用
typeof
操作符
Object Member Listing
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();
}
Structs
结构体和类很相似,不同点在于
struct
是一个值类型,类是一个引用类型struct
不支持继承- 因为
struct
是值类型,所以是没有null的,它的默认值是空的结构体
Read-Only Structs and Functions
readonly struct Point
{
public int X, Y;
}
可以在结构体上标注只读,这样结构体所有字段都是只读的,也可以给方法标注只读,这样方法如果想去修改任何字段,都会报编译时错误
Ref Structs
不像那些引用类型,都是存活在堆中的,结构体的内存是根据变量声明的位置决定的。如果一个值类型出现在参数或者本地变量中,那么它是存活在栈中的
但是如果值类型是出现在类的字段中,那它也是在堆中存活的
如果给结构体添加ref
关键字,那么这个结构体只能存活在栈中
Access Modifiers
- public,公开的,所有assembly都可以访问,它是enum或者interface的默认访问
- internal,只能是同一个assembly的类型才能访问,这是默认的非嵌套类型访问
- private,私有的,除了类型自己,其他都不能访问
- protected,只能类型或者类型的子类可以访问
- protected internal,是
internal
和protected
的结合 - private protected,只能同一个assembly的子类才能访问
Friend Assemblies
可以暴露internal
的成员给friendassembly访问,通过使用attribute来实现System.Runtime.CompilerServices.InternalsVisibleTo
[assembly: InternalsVisibleTo ("Friend")]
Interfaces
接口就像是无状态的类
- 接口只能定义方法,没有字段
- 接口的成员默认是abstract
- 类或者结构体可以实现多个接口
Extending an Interface
接口可以继承其他接口
Explicit Interface Implementation
当两个接口具有相同的方法签名的时候,我们可以显式实现某一个接口的方法
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
Implementing Interface Members Virtually
一个接口的成员实现,默认是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");
}
Reimplementing an Interface in a Subclass
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
的,所以子类不能覆盖,子类必须实现接口并重新实现
Default Interface Members
C# 8增加了,默认方法作为接口的成员
Enums
枚举是一个特殊的类型,它是一组被命名的数字常量。
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 }
Enum Conversions
枚举可以转换成一个数字类型,反向操作也是可以的。
int i = (int) BorderSide.Left;
BorderSide side = (BorderSide) i;
bool leftOrRight = (int) side <= 2;
Flags Enums
也可以将枚举的成员结合在一起,防止歧义,成员的结合需要显式赋值
[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
}
Type-Safety Issues
枚举的真实值是数字,那么在转换的时候,可以用一个没有定义的值转换,不会发生错误,可以使用方法Enum.IsDefined
进行验证
BorderSide side = (BorderSide) 12345;
Console.WriteLine (Enum.IsDefined (typeof (BorderSide), side)); // False
Generics
C#有两种机制,来使得不同的类型重用代码:继承和范型
继承表达的是重用一个基础类型,范型表达的是重用一个模板包含着一个占位符类型。范型和继承比较,它增加了类型安全和减少强转和装箱
Generic Types
范型类型声明一个类型参数,它是一个占位符,会在使用的地方填上具体的类型。
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>
叫做封闭类型。在运行时,所有的范型类型实例都是封闭的
typeof and Unbound Generic Types
运行时是不存在开放范型类型的。它们在编译时就变成封闭类型了。然而,还有一种可能是,运行时存在不受约束的范型类型,纯粹作为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); } }
The default Generic Value
可以用default
关键字区获取范型类型参数的默认值,如果是引用类型的默认值是null,如果是值类型的默认值就是按位置零
Generic Constraints
范型约束,约束可以应用在类型参数中,具有更多指定的类型参数
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 // 不可空值类型或者不可空引用类型
Covariance
假如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);
Declaring a covariant type parameter
可以在接口或者委托上声明一个协变类型参数,通过将它标记为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
修饰符保证了这个类型参数只会出现在输出参数中,而不会输入
Contravariance
逆变,是协变的逆转。比如说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# Generics Versus C++ Templates
C#的范型和C++的模板非常相似,但是内部运行的机制是非常不一样的。两个都是必须是声明的和实际使用的结合在一起,声明的占位符必须让使用者填上。然而,C#的范型是可以编译成库的,因为声明和使用的结合成封闭类型是一直到运行时才会发生。但是C++模板的话,是发生在编译时的。这意味着C++不能发布模板库,它们只会存在源码中。
原创文章,作者:254126420,如若转载,请注明出处:https://blog.ytso.com/271126.html