一、类
类是最常见的一种引用类型
class Test{}//声明了一个简单的类Test
复杂的类:
- 在class关键字之前:类特性(Attribute)和类修饰符。非嵌套的类修饰符有:public、internal、abstract、sealed、static、unsafe和partial。
- 紧接类名:泛型参数、唯一基类与多个接口。
- 在花括号内:类成员(方法、属性、索引器、事件、字段、构造器、重载运算符、嵌套类型和终结器)
1.1、字段
字段是类或结构体中的变量成员。
class Test{
int i;
public int a;
}
字段可用以下修饰符进行修饰
- 静态修饰符:static
- 访问权限修饰符:public internal private protected
- 继承修饰符:new
- 不安全代码修饰符:unsafe
- 只读修饰符:readonly
- 线程访问修饰符:volatile
readonly只读修饰符
readonly修饰符防止字段在构造后进行变更。只读字段只能在声明时或在其所属的类型构造器中赋值。
class Test1
{
public readonly int i;
public Test1(int i)
{
this.i = i;
}
}
class Test2
{
static void Main()
{
Test1 test = new Test1(10);
// test.i = 10; 当加上readonly修饰符后不能进行这样的赋值
Console.WriteLine(test.i);
}
}
字段初始化
字段不一定要初始化。没有初始化的字段均为默认值(0、\0、null、false)。字段初始化逻辑在构造器之前运行。
也可以同时声明多个字段,用逗号分隔开
class Test{
int i,j;
}
1.2、方法
方法用一组语句实现某个行为。方法从调用者指定的参数中获得输入数据,并通过指定的输出类型将输出数据返回给调用者。方法可以返回void类型,表明它不会向调用者返回任何值。此外,方法还可以通过ref/out参数向调用者返回输出数据。
方法的签名在这个类型的签名中必须是唯一的。方法的签名由它的名字和一定顺序的参数类型(但不包含参数名和返回值类型)组成。
方法可以用以下修饰符修饰
- 静态修饰符:static
- 访问权限修饰符:public internal private protected
- 继承修饰符:new virtual abstract override sealed
- 部分方法修饰符:partial
- 非托管代码修饰符:unsafe extern
- 异步代码修饰符:async
表达式方法
//方法体只有一个表达式
int add(int x,int y){ return x+y;}
//简写
int add(int x,int y) => x+y;
方法重载
方法名相同,参数列表或类型不同就是方法重载
int add(int x,int y){ }
int add(int x){ }
int add(double x){ }
加上params修饰符不属于参数类型不同,不可以重载
void re(int[] x) { }
void re(params int[] y){ }//编译错误
ref out 不能同时出现在类中
void re(int x) { }
void re(ref int x){ }
void re(out int x){ }//编译错误
局部方法(C#7)
方法中定义另一个方法为局部方法
int print(int x){
int add(int y)=>x+y;
}
注意
局部方法仅仅在包含它的方法范围内可见。
局部方法可以访问父方法中的局部变量和参数。
局部方法还可以出现在其他的类型的函数中,例如属性访问器和构造器。
局部方法可以是迭代的和异步的。
局部方法不能用static修饰。如果父方法是静态的,那么局部方法也是隐式静态的。
1.3、实例构造器
构造器执行类或结构体的初始化代码。构造器的定义和方法的定义类似,区别仅在于构造器名和返回值只能和封装它的类型相同。
public class Person{
string name;
public Person (string name){
this.name=name;
}
}
Person p = new Person("张三");
实例构造器支持以下的修饰符
- 访问权限修饰符:public internal private protected
- 非托管代码修饰符:unsafe extern
重载构造器
类或者结构体可以重载构造器。为了避免重复代码,构造器可以用this关键字调用另一个构造器。
class Test1
{
public readonly int i;
public Test1(int i): this("name")//需要加上冒号和this
{
this.i = i;
}
public Test1(string name)
{
Console.WriteLine(name);
}
}
注意
当构造器调用另一个构造器时,被调用的构造器先执行。
表达式内不能使用this引用,例如,不能调用实例方法(这是强制性的。由于这个对象当前还没有通过构造器初始化完毕,因此调用任何方法都有可能失败)。但是表达式可以调用静态方法。
编译器会自动为没有显式定义构造器的类生成无参数公有构造器。但是,一旦显式定义了至少一个构造器,系统就不再自动生成无参数的构造器。
字段的初始化按声明的先后顺序,在构造器之前执行。
非公有构造器
构造器不一定都是公有的。通常,定义非公有的构造器的原因是为了通过一个静态方法调用来控制类实例的创建。静态方法可以从一个池中返回对象,而不必每次创建一个新对象的实例。静态方法还可以根据不同的输入参数返回不同的子类对象。
class Test1
{
public readonly int i;
Test1(int i)
{
this.i = i;
}
static Test1 Create()
{
return new Test1(10);
}
}
Test1 t1 = Test1.Create();
解构器(C#7)
一个解构器(或称之为解构方法)就像构造器的反过程:构造器使用若干值作为参数,并且将它们赋值给字段;而解构器则相反将字段反向赋值给若干变量。
解构方法的名字必须为Deconstruct,并且拥有一个或多个out参数。
class Rectangle
{
public readonly float Width, Height;
public Rectangle (float width, float height)
{
Width = width;
Height = height;
}
public void Deconstruct (out float width, out float height)
{
width = Width;
height = Height;
}
}
var rect = new Rectangle (3, 4);
(float width, float height) = rect; // 结构调用,声明了两个变量
//等价上面一行
float width, height;
rect.Deconstruct (out width, out height);
Console.WriteLine (width + " " + height); // 3 4
1.4、对象初始化器
为了简化对象的初始化,可以在调用构造器之后直接通过对象初始化器设置对象的可访问字段或属性。
public class Bunny
{
public string Name;
public bool LikesCarrots;
public bool LikesHumans;
public Bunny () {}
public Bunny (string n) { Name = n; }
}
//就可以用对象初始化器对Bunny对象进行实例化
Bunny b1 = new Bunny { Name="Bo", LikesCarrots=true, LikesHumans=false };
Bunny b2 = new Bunny ("Bo") { LikesCarrots=true, LikesHumans=false };
1.5、this引用
this引用指代实例本身。
public class Panda
{
public Panda Mate;
public void Marry (Panda partner)
{
Mate = partner;
partner.Mate = this;
}
}
this引用可避免字段、局部变量或属性之间发生混淆。
public class Test
{
string name;
public Test (string name) { this.name = name; }
}
this引用仅在类或结构体的非静态成员中有效。
1.6、属性
属性(Property)和字段很类似,但是属性内部像方法一样含有逻辑。
属性和字段的声明很类似,但是属性比字段多出了get/set代码块。
class Test1
{
int i;//私有
public int I
{
get{ return 10; }
set{ i= value; }
}
}
get和set是属性的访问器。读属性时会运行get访问器,它必须返回属性类型的值。给属性赋值时运行set访问器。它有一个名为value的隐含参数,其类型和属性的类型相同。它的值一般来说会赋值给一个私有字段。
尽管访问属性和字段的方式是相同的,但不同之处在于,属性在获取和设置值的时候给实现者提供了完全的控制能力。这种控制能力使实现者可以选择任意的内部表示形式,而无须将属性的内部细节暴露给用户。
属性支持以下的修饰符:
- 静态修饰符:static
- 访问权限修饰符:public internal private protected
- 继承修饰符:new virtual abstract override sealed
- 非托管代码修饰符:unsafe extern
只读属性和计算属性
如果只定义了get访问器,属性就是只读的。如果只定义了set访问器,那么它就是只写的。一般很少使用只写属性。
通常属性会用一个专门的后台字段来存储其所代表的数据,但属性也可以从其他数据计算得来。
decimal currentPrice, sharesOwned;
public decimal Worth
{
get { return currentPrice * sharesOwned; }
}
表达式属性(C# 6和C# 7)
从C# 6开始,只读属性(就像之前的例子中那样的属性)可简写为表达式体属性。它使用双箭头替换了花括号、get访问器和return关键字。
public decimal Worth => currentPrice * sharesOwned;
C# 7进一步允许在set访问器上使用表达式体。其书写方法如下:
public decimal Worth
{
get => currentPrice * sharesOwned;
set => sharesOwned = value / currentPrice;
}
自动属性
属性最常见的实现方式是使用get访问器/set访问器读写私有字段(该字段与属性类型相同)。因而编译器会将自动属性声明自动转换为这种实现方式。
class Test1
{
int i;
public int I
{get; set; }
}
编译器会自动生成一个后台私有字段,该字段的名称由编译器生成且无法引用。如果希望属性对其他类型暴露为只读属性,则可以将set访问器标记为private或protected。自动属性是在C# 3.0中引入的。
属性初始化器(C# 6)
C# 6开始支持自动属性的初始化器。其写法就像初始化字段一样:
public decimal CurrentPrice { get; set; } = 123;
上述写法将CurrentPrice的值初始化为123。拥有初始化器的属性可以为只读属性:
public int Maximum { get; } = 999;
就像只读字段那样,只读自动属性只可以在类型的构造器中赋值。这个功能适于创建不可变(只读)的对象。
CLR属性的实现
C#属性访问器在内部会编译为名为get_XXX和set_XXX的方法:
public decimal get_CurrentPrice {...}
public void set_CurrentPrice (decimal value) {...}
简单的非虚属性访问器会被JIT(即时)编译器内联编译,消除了属性和字段访问间的性能差距。内联是一种优化方法。它用方法的函数体替代方法调用。
对于WinRT属性,编译器会将put_XXX作为属性命名约定而非set_XXX。
1.7、索引器
索引器为访问类或者结构体中封装的列表或字典型数据元素提供了自然的访问接口。索引器和属性很相似,但索引器通过索引值而非属性名称访问数据元素。例如string类具有索引器,可以通过int索引访问其中每一个char的值。
string s = "hello";
Console.WriteLine (s[0]); // 'h'
Console.WriteLine (s[3]); // 'l'
索引器的实现
编写索引器首先要定义一个名为this的属性,并将参数定义放在一对方括号中。
class Sentence
{
string[] words = "The quick brown fox".Split();
public string this [int wordNum] // indexer
{
get { return words [wordNum]; }
set { words [wordNum] = value; }
}
}
Sentence s = new Sentence();
Console.WriteLine (s[3]); // fox
s[3] = "kangaroo";
Console.WriteLine (s[3]); // kangaroo
一个类型可以定义多个参数类型不同的索引器,一个索引器也可以包含多个参数:
public string this [int arg1, string arg2]
{
get { ... } set { ... }
}
如果省略set访问器,则索引器就是只读的,并且可以使用C# 6的表达式语法来简化定义。
CLR索引器的实现
索引器在内部会编译为名为get_Item和set_Item的方法,如下所示:
public string get_Item (int wordNum) {...}
public void set_Item (int wordNum, string value) {...}
1.8、常量
常量是一种值永远不会改变的静态字段。常量会在编译时静态赋值,编译器会在常量使用点上直接替换该值(类似于C++的宏)。常量可以是内置的数据类型:bool、char、string或者枚举类型。常量用关键字const声明,并且必须用值初始化。
public class Test
{
public const string Message = "Hello World";
}
常量在使用时比起static readonly字段有着更多的限制,不仅能够使用的类型有限,而且初始化字段的语句含义也不同。其他的不同之处还有常量是在编译时进行赋值的。
public static double Circumference (double radius)
{
return 2 * System.Math.PI * radius;
}
//编译为
public static double Circumference (double radius)
{
return 6.2831853071795862 * radius;
}
这样做是合理的。因为PI是常量,它的值永远不变。相反,static readonly字段可以在每一个应用程序中有不同的值。
static readonly字段的好处还在于当提供给其他程序集时,可以在后续版本中更新其数值
例如
//提供了一个常量
public const int i = 10;
别的程序集引用了该常量,那么当前程序集如果将I值改变为11,那么别的程序集还是使用的10,除非重新编译,所以常量就应该是一直不变,static readonly 就没有这个问题。
非局部常量可以使用以下的修饰符:
- 访问权限修饰符:public internal private protected
- 继承修饰符:new
1.9、静态构造器
每个类型的静态构造器只会执行一次,而不是每个实例执行一次。一个类型只能定义一个静态构造器,名称必须和类型同名,且没有参数:
class Test
{
static Test() { Console.WriteLine ("Type Initialized"); }
}
运行时将在类型使用之前调用静态构造器,以下两种行为可以触发静态构造器执行:
- 实例化类型
- 访问类型的静态成员
静态构造器只支持两个修饰符:unsafe和extern。
如果静态构造器抛出了未处理的异常,则类型在整个应用程序生命周期内都是不可用的。
静态构造器和字段初始化顺序
静态字段初始化器会在调用静态构造器前运行。如果类型没有静态构造器,字段会在类型被使用之前或者在运行时中更早的时间进行初始化。静态字段初始化器按照字段声明的先后顺序运行。
下面例子中X初始化为0而Y初始化为3。如果我们交换两个字段初始化顺序,两个字段都将初始化为3。
class Foo
{
public static int X = Y; // 0
public static int Y = 3; // 3
}
以下例子先打印0再打印3,因为实例化Foo在前,X在后,交换那两句后,都打印3。
class Program
{
static void Main() { Console.WriteLine (Foo.X); } // 3
}
class Foo
{
public static Foo Instance = new Foo();
public static int X = 3;
Foo() { Console.WriteLine (X); } // 0
}
1.10、静态类
类可以标记为static,表明它必须只能够由static成员组成,并不能够派生子类。System.Console和System.Math类就是静态类的最好示例。
1.11、终结器
终结器(Finalizer)是只能够在类中使用的方法。该方法在垃圾回收器回收未引用的对象占用的内存前调用。终结器的语法是类型的名称加上~前缀。
class Class1
{
~Class1()
{
...
}
}
事实上,这是C#语言重写Object类的Finalize方法的语法。编译器会将其扩展为如下的声明:
protected override void Finalize()
{
...
base.Finalize();
}
终结器允许使用以下的修饰符:
- 非托管代码修饰符:unsafe
1.12、分部类型和方法
分部类型(Partial type)允许一个类型分开进行定义,典型的做法是分开在多个文件中。分部类型使用的常见场景是从其他源文件自动生成分部类(例如从Visual Studio模板或设计器),而这些类仍然需要额外手动编写方法。
// PaymentFormGen.cs - auto-generated
partial class PaymentForm { ... }
// PaymentForm.cs - hand-authored
partial class PaymentForm { ... }
每一个部分必须包含partial声明。
partial class PaymentForm {}
class PaymentForm {}//错误
分部类型的各个组成部分不能包含冲突的成员,例如具有相同参数的构造器。分部类型完全由编译器处理,因此各部分在编译时必须可用,并且必须编译在同一个程序集中。可以在多个分部类声明中指定基类,只要基类是同一个基类即可。此外,每一个分部类型组成部分可以独立指定实现的接口。编译器并不保证分部类型声明中各个组成部分之间的字段初始化顺序。
分部方法
分部类型可以包含分部方法(partial method)。这些方法能够令自动生成的分部类型为手动编写的代码提供自定义钩子(hook)。
partial class PaymentForm // In auto-generated file
{
...
partial void ValidatePayment (decimal amount);
}
partial class PaymentForm // In hand-authored file
{
...
partial void ValidatePayment (decimal amount)
{
if (amount > 100)
...
}
}
分部方法由两部分组成:定义和实现。定义一般由代码生成器生成,而实现一般由手动编写。如果没有提供方法的实现,分部方法的定义会被编译器清除(调用它的代码部分也一样)。这样,自动生成的代码既可以提供钩子又不必担心代码过于臃肿。分部方法返回值类型必须是void,且默认是private的。
1.13、nameof运算符(C#6)
nameof运算符返回任意符号的字符串名称(类型、成员、变量等)。
int count = 123;
string name = nameof (count); // name is "count"
当指定一个类型的成员(例如属性和字段)名称时,请务必引用其类型名称。
Console.WriteLine(nameof (StringBuilder.Length));//Length
二、继承
类可以通过继承另一个类来对自身进行扩展或定制。继承类可以重用其所有功能而无须重新构建。类只能继承自唯一的类,但是可以被多个类继承,从而形成了类的层次。
class Base{
public string Name;
}
class Base1 : Base
{
public string desc;
}
class Base2:Base
{
public int age;
}
class Test2
{
static void Main()
{
Base1 base1 = new Base1 { Name="张三",desc="123"};
Base2 base2 = new Base2() { Name = "李四", age = 12 };
Console.WriteLine(base1.Name);
Console.WriteLine(base1.desc);
Console.WriteLine(base2.Name);
Console.WriteLine(base2.age);
}
}
2.1、多态
引用是多态的。意味着x类型的变量可以指向x子类的对象。父类引用指向子类对象
class Base{
public string Name;
}
class Base1 : Base
{
public string desc;
}
class Base2:Base
{
public int age;
}
class Test2
{
static void Main()
{
Base base1 = new Base1 { Name="张三",desc="123"};//父类类型接收子类对象
Base base2 = new Base2() { Name = "李四", age = 12 };
Console.WriteLine(base1.Name);
Console.WriteLine(((Base1)base1).desc);//对子类进行强转向下转型
Console.WriteLine(base2.Name);
Console.WriteLine(((Base2)base2).age);
}
}
2.2、类型转换和引用转换
对象引用可以:
- 隐式向上转换为基类的引用;
- 显式向下转换为子类的引用。
各个兼容的类型的引用之间向上或向下类型转换仅执行引用转换:(逻辑上)生成一个新的引用指向同一个对象。向上转换总是能够成功,而向下转换只有在对象的类型符合要求时才能成功。
向上类型转换
向上类型转换创建一个基类指向子类的引用。
Base1 bas = new Base1();
Base base=bas;
向上转换之后,变量base仍然是bas指向的Base对象。被引用的对象本身不会被替换或者改变。
Console.WriteLine (base == bas); // True
Console.WriteLine (base.Name);
Console.WriteLine (base.desc); //报错 因为Base类型没有这个属性 需要访问必须向下转型
向下类型转换
向下转换是从基类引用创建一个子类引用。
Base1 bas = new Base1();
Base base=bas;
Base1 bs1=(Base1)base;
Console.WriteLine(bs1.desc);
向上转换仅仅影响引用,而不会影响被引用的对象。而向下转换必须是显式转换,因为它有可能导致运行时错误:
Base2 bas = new Base2();
Base base=bas;
Base1 bs1=(Base1)base;//报错
如果向下转换失败,会抛出InvalidCastException。
as运算符
as运算符在向下类型转换出错时返回null(而不是抛出异常)
Base base = new Base();
Base1 bs = base as Base1;//bs = null 不会抛异常
if (bs ! = null) Console.WriteLine (bs.desc);
as运算符不能用来实现自定义转换,也不能用于数值的转换。
as和类型转换运算符也可以用来实现向上类型转换,但是不常用。
is运算符
is运算符检查引用的转换是否能够成功,即对象是否从某个特定的类派生(或是实现某个接口)。该运算符常在向下类型转换前使用。
if (bs is Base1)
Console.WriteLine (((Base1)bs).desc);
如果拆箱转换(unboxing conversion)能成功执行,则is运算符也会返回true,但它不能用于自定义类型转换和数值转换。
is运算符和模式变量(C# 7)
从C# 7开始我们可以在使用is运算符的同时引入一个变量。
if (a is Base1 s)
Console.WriteLine (s.desc);
等价于下面
Base1 s;
if (bs is Base1)
{
s = (Base1) bs;
Console.WriteLine (s.desc);
}
2.3、虚函数成员
提供特定实现的子类可以重写(overridden)标识为virtual的函数。方法、属性、索引器和事件都可以声明为virtual:
public class Asset
{
public string Name;
public virtual decimal Liability => 0;
}
子类通过override修饰符重写虚方法
public class Stock : Asset
{
public long SharesOwned;
}
public class House : Asset
{
public decimal Mortgage;
public override decimal Liability => Mortgage;
}
House mansion = new House { Name="McMansion", Mortgage=250000 };
Asset a = mansion;
Console.WriteLine (mansion.Liability); // 250000
Console.WriteLine (a.Liability); // 250000
虚方法和重写的方法的签名、返回值以及可访问性必须完全一致。重写方法可以通过base关键字调用其基类的实现。
从构造器调用虚方法有潜在的危险性,因为编写子类的人在重写方法的时候未必知道现在正在操作一个未完全实例化的对象。换言之,重写的方法很可能最终会访问到一些方法或属性,而这些方法或属性依赖的字段还未被构造器初始化。
class Base{
public string Name;
public Base()
{
Console.WriteLine(Age);
}
public virtual int Age { get { return 10; } }
}
class Base2 : Base
{
int age;
public override int Age { get { return age; } }
public Base2(){
age = 12;
Console.WriteLine(Age);
}
}
class Test2
{
static void Main()
{
Base base2 = new Base2();// 0 12
//重写了Age 当new Base2()时先调用父类的构造,父类构造打印了age属性,因为new Base2()是子类,所以父类构造中打印的age是子类的age属性,此时子类的age属性还未通过构造器初始化,所以是默认值0 ,父类调用完成后,再执行子类构造器,此时就进行了age的初始化,打印了12
}
}
2.4、抽象类和抽象成员
声明为抽象(abstract)的类不能够实例化,只有抽象类的具体实现子类才能实例化。抽象类中可以定义抽象成员,抽象成员和虚成员相似,只不过抽象成员不提供默认的实现。除非子类也声明为抽象类,否则其实现必须由子类提供。
abstract class Base{
public string Name;
abstract public int Age { get; }
}
class Base2 : Base
{
int age;
public override int Age { get { return age; } }
public Base2(){
age = 12;
Console.WriteLine(Age);
}
}
2.5、隐藏继承成员
基类和子类可能定义相同的成员。
public class A { public int Counter = 1; }
public class B : A { public int Counter = 2; }
类B中的Counter字段隐藏了类A中的Counter字段。通常,这种情况是在定义了子类成员之后又意外地将其添加到了基类中而造成的。因此,编译器会产生一个警告,并采用下面的方法避免这种二义性:
- A的引用(在编译时)绑定到A.Counter
- B的引用(在编译时)绑定到B.Counter有时需要故意隐藏一个成员。此时可以在子类的成员上中使用new修饰符。new修饰符仅用于阻止编译器发出警告。
public class A { public int Counter = 1; }
public class B : A { public new int Counter = 2; }
new修饰符可以明确将你的意图告知编译器和其他开发者:重复的成员是有意义的。
new和重写
public class BaseClass
{
public virtual void Foo() { Console.WriteLine ("BaseClass.Foo"); }
}
public class Overrider: BaseClass
{
public override void Foo() { Console. WriteLine ("Overrider.Foo"); }
}
public class Hider : BaseClass
{
public new void Foo() { Console.WriteLine ("Hider.Foo"); }
}
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
重写不论是父类引用还是子类引用,调用的都是子类的重写后的方法。
new 如果是子类引用,调用子类方法,父类引用,调用父类方法。
如果是字段,则谁引用,调用谁的。
2.6、密封函数和类
重写的函数成员可以使用sealed关键字密封其实现,防止其他的子类再次重写。
public sealed override int Age { get { return age; } }//防止该类的子类再次重写该方法。
也可以在类中使用sealed修饰符来密封整个类,这会隐式地密封类中所有的虚函数。密封类比密封函数成员更常见。
2.7、base关键字(类似java super)
base关键字和this关键字很相似。它有两个重要目的:
- 从子类访问重写的基类函数成员
- 调用基类的构造器
public class House : Asset
{
...
public override decimal Liability => base.Liability + Mortgage;
}
通过base关键字,我们用非虚的方式访问Asset的Liability属性。这意味着不管实例的运行时类型如何,都将访问Asset类的相应属性。如果Liability是隐藏属性而非重写的属性,该方法也同样有效。(也可以在调用相应函数前,将其转换为基类来访问隐藏的成员。)
2.8、构造器和继承
子类必须声明自己的构造器。派生类可以访问基类的构造器,但是并非自动继承。构造器不能继承。
base关键字可以访问父类构造,但这不是因为继承原因。
基类的构造器总是先执行,这保证了基类的初始化发生在子类特定的初始化之前。
隐式调用基类的无参数构造器
如果子类的构造器省略base关键字,那么基类的无参数构造器将被隐式调用。
public class BaseClass
{
public int X;
public BaseClass() { X = 1; }
}
public class Subclass : BaseClass
{
public Subclass() { Console.WriteLine (X); } // 1
}
如果基类没有可访问的无参数的构造器,子类的构造器中就必须使用base关键字。
构造器和字段初始化的顺序
当对象实例化时,初始化按照以下的顺序进行:
- 从子类到基类a)初始化字段b)计算被调用的基类构造器中的参数
- 从基类到子类a)构造器方法体的执行
public class B
{
int x = 1; // Executes 3rd
public B (int x)
{
... // Executes 4th
}
}
public class D : B
{
int y = 1; // Executes 1st
public D (int x)
: base (x + 1) // Executes 2nd
{
... // Executes 5th
}
}
2.9、重载和解析
继承对方法的重载有着特殊的影响。
static void Foo (Asset a) { }
static void Foo (House h) { }
当重载被调用时,类型最明确的优先匹配
House h = new House (...);
Foo(h); // Calls Foo(House)
具体调用那个重载是在编译器静态决定的而非运行时决定。
Asset a = new House (...);
Foo(a); // Calls Foo(Asset)
如果把Asset类转换为dynamic,则会在运行时决定调用哪个重载。这样就会基于对象的实际类型进行选择。
Asset a = new House (...);
Foo ((dynamic)a); // Calls Foo(House)
三、Object
object类型(System.Object)是所有类型的最终基类。任何类型都可以向上转换为object类型。
object[] o = new object[10];
o[0] = "aaa";
o[1] = 111;
o[2] = new int[10];
Console.WriteLine(o[0]);
Console.WriteLine(o[1]);
Console.WriteLine(o[2]);
当值类型和object类型相互转换时,公共语言运行时(CLR)必须进行一些特定的工作来对接值类型和引用类型在语义上的差异。这个过程称为装箱(boxing)和拆箱(unboxing)。
3.1、装箱和拆箱
装箱是将值类型实例转换为引用类型实例的行为。引用类型可以是object类或接口。
拆箱操作刚好相反,它把object类型转换成原始的值类型。
int x = 9;
object obj = x; //装箱
int y = (int)obj; //拆箱
拆箱需要显式类型转换。运行时将检查提供的值类型和真正的对象类型是否匹配,并在检查出错的时候抛出InvalidCastException。
object obj = 9;
long x = (long) obj; // InvalidCastException
装箱转换对系统提供一致性的数据类型至关重要。
数组和泛型的变量只能支持引用转换,不能支持装箱转换。
object[] a1 = new string[3];
object[] a2 = new int[3]; // 报错
3.2、静态和运行时类型检查
C#程序在静态(编译时)和运行时(CLR)都会执行类型检查。
静态类型检查使编译器能够在程序没有运行的情况下检查程序的正确性。例如,因为编译器会强制进行静态类型检查因而以下代码会出错。
int a="aaa";//编译期间就错了
在使用引用类型转换或者拆箱操作进行向下类型转换时,CLR会执行运行时类型检查。
object i = "aa";
int a = (int)i;//运行时报错
运行时可以进行类型检查是因为堆上的每一个对象都在内部存储了类型标识,这个标识可以通过调用object类的GetType方法得到。
3.3、GetType方法和typeof运算符
C#中的所有类型在运行时都会维护System.Type类的实例。
有两个基本方法可以获得System.Type对象:
- 在类型实例上调用GetType方法
- 在类型名称上使用typeof运算符
GetType在运行时计算而typeof在编译时静态计算(如果是用泛型类型参数,那么它将由即时编译器解析)。
System.Type拥有诸多属性,例如类型的名称、程序集、基类型等属性。
System.Type同时还是运行时反射模型的访问入口。
using System;
public class Point { public int X, Y; }
class Test
{
static void Main()
{
Point p = new Point();
Console.WriteLine (p.GetType().Name); // Point
Console.WriteLine (typeof (Point).Name); // Point
Console.WriteLine (p.GetType() == typeof(Point)); // True
Console.WriteLine (p.X.GetType().Name); // Int32
Console.WriteLine (p.Y.GetType().FullName); // System.Int32
}
}
3.4、ToString方法
ToString方法返回类型实例的默认文本描述。所有内置类型都重写了该方法。
int x = 1;
string s = x.ToString(); // s is "1"
class Base2
{
public override string ToString()
{
return "我重写了Tostring方法";
}
}
Base2 bas = new Base2();
Console.WriteLine(bas.ToString());//我重写了Tostring方法
如果不重写ToString方法,那么它会返回类型的名称。
当直接在值类型对象上调用ToString这样的object成员时,若该成员是重写的则不会发生装箱。只有进行类型转换时才会执行装箱操作:
int x = 1;
string s1 = x.ToString(); // Calling on nonboxed value
object box = x;
string s2 = box.ToString(); // Calling on boxed value
3.5、object的成员列表
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();
}
四、结构体
结构体和类相似,不同之处在于:
- 结构体是值类型,而类是引用类型。
- 结构体不支持继承(除了隐式派生自object类型,或更精确地说,是派生自System. ValueType)。
除了以下内容,结构体可以包含类的所有成员: - 无参数的构造器
- 字段初始化器
- 终结器
- 虚成员或protected成员
当表示一个值类型语义时,使用结构体更加理想。数值类型就是一个很好的例子。对于数值来说,在赋值时对值进行复制而不是对引用进行复制是很自然的。由于结构体是值类型,因此其实例不需要在堆上实例化,创建一个类型的多个实例就更加高效了。例如,创建一个值类型的数组只需要进行一次堆空间的分配。
结构体的构造语义如下:
- 结构体隐式包含一个无法重写的无参数构造器,将字段按位置为0。
- 定义结构体的构造器时,必须显式为每一个字段赋值。
public struct Point
{
int x, y;
public Point (int x, int y) { this.x = x; this.y = y; }
}
...
Point p1 = new Point (); // p1.x and p1.y will be 0
Point p2 = new Point (1, 1); // p1.x and p1.y will be 1
五、访问权限修饰符
为了提高封装性,类型或类型成员可以在声明中添加以下五个访问权限修饰符之一来限定其他类型和其他程序集对它的访问。
- public:完全访问权限。枚举类型成员或接口成员隐含的可访问性。
- internal:仅可以在程序集内访问,或供友元程序集访问。这是非嵌套类型的默认可访问性。
- private:仅可以在包含类型中访问。这是类或者结构体成员的默认可访问性。
- protected:仅可以在包含类型或子类中访问。
- protected internal:protected和internal可访问性的并集。Eric Lippert是这样解释的:默认情况下尽可能将一切规定为私有,然后每一个修饰符都会提高其访问级别。所以用protected internal修饰的成员在两个方面的访问级别都提高了。
class Class1 {}
public class Class2 {}//Class2可以从本程序集外访问,而Class1不可以
class ClassA { int x; }
// ClassB的字段x可以被本程序集的其他类型访问,而ClassA的则不可以
class ClassB { internal int x; }
//Subclass中的函数可以可以调用Bar但是不能调用Foo
class BaseClass
{
void Foo() {}
protected void Bar() {}
}
class Subclass : BaseClass
{
void Test1() { Foo(); }
void Test2() { Bar(); }
}
5.1、友元程序集
在一些高级的场景中,添加System.Runtime.CompilerServices.InternalsVisibleTo程序集特性就可以将internal成员提供给其他的友元程序集访问。
可以用如下方法指定友元程序集:
[assembly: InternalsVisibleTo ("Friend")]
如果友元程序集有强名称,必须指定其完整的160字节公钥:[assembly: InternalsVisibleTo (“StrongFriend, PublicKey=0024f000048c…”)]可以使用LINQ查询的方式从强命名的程序集中提取完整的公钥值:
string key = string.Join ("",
Assembly.GetExecutingAssembly().GetName().GetPublicKey()
.Select (b => b.ToString ("x2")));
5.2、可访问性封顶
类型的可访问性是它内部声明成员可访问性的封顶。关于可访问性封顶,最常用的示例是internal类型中的public成员。
class C { public void Foo() {} }
C的(默认)可访问性是internal,它作为Foo的最高访问权限,使Foo成为internal的。而将Foo指定为public的原因一般是为了将来将C的权限改成public时重构的方便。
5.3、访问权限修饰符的限制
当重写基类的函数时,重写函数的可访问性必须一致。
class BaseClass { protected virtual void Foo() {} }
class Subclass1 : BaseClass { protected override void Foo() {} }
class Subclass2 : BaseClass { public override void Foo() {} } // 报错
(若在另外一个程序集中重写protected internal方法,则重写方法必须为protected。这是上述规则中的一个例外情况。)编译器会阻止任何不一致的访问权限修饰符。例如,子类可以比基类的访问权限低,但不能比基类的访问权限高:
internal class A {}
public class B : A {} // 报错
六、接口
接口和类相似,但接口只为成员提供定义而不提供实现。
接口与类的不同点:
- 接口的成员都是隐式抽象的。相反,类可以包含抽象的成员和有具体实现的成员。
- 一个类(或者结构体)可以实现多个接口。而一个类只能够继承一个类,结构体则完全不支持继承(只能从System.ValueType派生)。
接口声明和类声明很相似。但接口不提供成员的实现,这是因为它的所有成员都是隐式抽象的。这些成员将由实现接口的类或结构体实现。接口只能包含方法、属性、事件、索引器,而这些正是类中可以定义为抽象的成员类型。
接口中的成员都是public的,不可以用别的权限修饰符修饰,因为接口是为所有实现他的类提供成员访问。
interface Inter
{
void Update();
}
class InterImpl : Inter
{
public void Update()//实现的方法必须为public
{
Console.WriteLine("修改成功");
}
}
6.1、扩展接口
接口也可以继承其他接口。
interface Inter
{
public void Update();
void delete();
}
interface UserInter : Inter
{
public void select();
}
只要实现了UserInter接口,就必须实现Inter接口和UserInter接口的所有方法。
6.2、显式接口实现
当实现多个接口时,有时会出现成员签名的冲突。显式实现(explicitly implementing)接口成员可以解决冲突。
interface Inter
{
void Update();
void select();
}
interface UserInter : Inter
{
void select();
}
class InterImpl : UserInter
{
public void Update()
{
Console.WriteLine("修改成功");
}
void UserInter.select(){}//方法名前面加上接口名就可以区分
void Inter.select(){}
}
Inter i = new InterImpl();
((Inter)i).select();//调用的时候需要先进行转型
((UserInter)i).select();
另一个使用显式实现接口成员的原因是隐藏那些高度定制化的或对类的正常使用干扰很大的接口成员。例如,实现了ISerializable接口的类通常会选择隐藏ISerializable成员,除非显式转换成这个接口。
显示实现接口的方法不能加修饰符,只能通过接口实现调用。
6.3、虚方法实现接口成员
默认情况下,隐式实现的接口成员是密封的。为了重写,必须在基类中将其标识为virtual或者abstract。
interface UserInter
{
void select();
}
class InterImpl : UserInter
{
public virtual void Update()
{
Console.WriteLine("InterImpl的update方法");
}
void select(){
Console.WriteLine("InterImpl 的select方法");
}
}
class InterChild : InterImpl
{
public override void Update()
{
Console.WriteLine("InterChild的update方法");
}
}
InterChild interChild = new InterChild();
interChild.Update();//InterChild的update方法
UserInter u = new InterChild(); //创建的是子类所以调用的是子类的
((InterImpl)u).Update();//InterChild的update方法
显式实现的接口成员不能标识为virtual,也不能实现通常意义的重写,但是它可以被重新实现(reimplemented)。
6.4、在子类中重新实现接口
子类可以重新实现基类实现的任意一个接口成员。不管基类中该成员是否为virtual,当通过接口调用时,重新实现都能够劫持成员的实现。它对接口成员的隐式和显式实现都有效,但后者效果更好。
public interface IUndoable { void Undo(); }
//TextBox显示接口实现了Undo方法
public class TextBox : IUndoable
{
void IUndoable.Undo() => Console.WriteLine ("TextBox.Undo");
}
//RichTextBox类再次重新实现Iundoable接口,重新实现他的Undo方法
public class RichTextBox : TextBox, IUndoable
{
public void Undo() => Console.WriteLine ("RichTextBox.Undo");
}
RichTextBox r = new RichTextBox();
r.Undo(); // RichTextBox.Undo
((IUndoable)r).Undo(); // RichTextBox.Undo
假定RichTextBox定义不变,如果TextBox隐式实现Undo
public class TextBox : IUndoable
{
public void Undo() => Console.WriteLine ("TextBox.Undo");
}
//因为都是实现的接口方法,转成谁就调用谁
RichTextBox r = new RichTextBox();
r.Undo(); // RichTextBox.Undo
((IUndoable)r).Undo(); // RichTextBox.Undo
((TextBox)r).Undo(); // TextBox.Undo
**通过重新实现来劫持调用的方式仅在通过接口调用成员时有效,而从基类调用时无效。**这个特性通常不尽人意,因为它有二义性。因此,重新实现主要适合于重写显式实现的接口成员。
接口重新实现的替代方法
即使是显式实现的成员,接口重新实现还是容易出问题,这是因为:
- 子类无法调用基类的方法
- 定义基类时不能预测方法是否会重新实现,或无法接受重新实现后的潜在问题
重新实现是子类未知时的最不理想的方法。更好的选择是在定义基类时不允许使用重新实现,有两种方法可以做到:
- 当隐式实现成员时,如果需要将其标记为virtual
- 当显式实现成员时,如果能够预测子类可能要重写某些逻辑,则使用下面的模式:
public class TextBox : IUndoable
{
void IUndoable.Undo() => Undo(); // Calls method below
protected virtual void Undo() => Console.WriteLine ("TextBox.Undo");
}
public class RichTextBox : TextBox
{
protected override void Undo() => Console.WriteLine("RichTextBox.Undo");
}
6.5、接口和装箱
将结构体转换为接口会引发装箱。而调用结构体的隐式实现接口成员不会引发装箱。
interface I { void Foo(); }
struct S : I { public void Foo() {} }
...
S s = new S();
s.Foo(); // 不进行装箱
I i = s; // 装箱
i.Foo();
6.6、什么时候使用类,什么时候使用接口?
个人理解:类是指的一类事物,他们有很多共同点,可以抽取出来。接口呢,制定的是规则,也可以制定的是行为。
举例:手机,这是一类事物,手机都可以打电话,发短信,还有很多相同的属性,例如品牌,价钱,外观等,可以抽成一个共同类,不同厂家来继承这个类,创建出各式各样的手机。比如飞机与鸟都可以飞,但是飞机和鸟不同,可以通过飞行接口来实现。
举例:接口制定规则,Java spring框架中事务的定义,因为数据库厂商不同,spring提供了接口,制定了规则,不同的数据库厂商必须实现基本的接口,接口中就制定了基本功能,必须需要实现的。
七、枚举类型和嵌套类型
枚举类型是一种特殊的值类型,可以在枚举类型中定义一组命名的数值常量。
public enum Seazon
{
Spring,Summer,Autumn,Winter
}
每一个枚举成员都对应一个整数。对应的数值是int类型,从0开始,按照声明顺序,01234…排下去。
可以指定其他的整数类型代替默认类型。
public enum Seazon:byte
{
Spring=1,Summer=10,Autumn=30,Winter=31
}
编译器还支持显式指定部分枚举成员。没有指定的枚举成员,在最后一个显式指定的值基础上递增。
7.1、枚举类型转换
枚举类型的实例可以与它对应的整数值相互显式转换。
public enum Seazon
{
Spring,Summer,Autumn,Winter
}
int i =(int) Seazon.Spring;
Console.WriteLine(i);//0
7.2、标志枚举类型
枚举类型的成员可以合并。为了避免混淆,合并枚举类型的成员要显式指定值,典型的值为2的幂次。
[Flags]
public 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特性。如果声明了一个没有标注Flags特性的枚举类型,其成员依然可以合并,但若在该枚举实例上调用ToString方法,则会输出一个数值而非一组名字。一般来说,合并枚举类型通常用复数名词而不用单数形式。
[Flags]
public enum BorderSides
{
None=0,
Left=1, Right=2, Top=4, Bottom=8,
LeftRight = Left | Right,
TopBottom = Top | Bottom,
All = LeftRight | TopBottom
}
7.3、枚举运算符
= == ! = < > <= > = + - ^ & | ~
+= -= ++ -- sizeof
位运算符、算术运算符和比较运算符都返回对应整数值的运算结果。枚举类型和整数类型之间可以做加法,但两个枚举类型之间不能做加法。
7.4、类型安全问题
public enum Seazon{Spring,Summer,Autumn,Winter }
//由于枚举类型可以和它对应的整数类型相互转换,因此枚举的真实值可能超出枚举类型成员的数值范围。
Seazon b = (Seazon) 12345;
Console.WriteLine (b); // 12345
Seazon b = Seazon.Spring;
b++;
Console.WriteLine(i);//Summer
显式检查枚举值的合法性。可以使用静态方法Enum.IsDefined来执行该操作
Seazon side = (Seazon) 12345;
Console.WriteLine (Enum.IsDefined (typeof (Seazon), side)); // False
Enum.IsDefined对标志枚举类型不起作用
下面的方法(巧妙使用了Enum.ToString()的行为)可以在标志枚举类型合法时返回true
static bool IsFlagDefined (Enum e)
{
decimal d;
//判断是否是数字,如果是数字返回true否则false
return ! decimal.TryParse(e.ToString(), out d);
}
[Flags]
public enum BorderSides { Left=1, Right=2, Top=4, Bottom=8 }
static void Main()
{
for (int i = 0; i <= 16; i++)
{
BorderSides side = (BorderSides)i;
Console.WriteLine (IsFlagDefined (side) + " " + side);
}
}
八、嵌套类型
嵌套类型(nested type)是声明在另一个类型内部的类型。一个类里又声明了一个类
class Test{
class Test1{}
}
嵌套类型的特征:
- 可以访问包含它的外层类型中的私有成员,以及外层类所能够访问的所有内容。
- 可以在声明上使用所有的访问权限修饰符,而不限于public和internal。
- 嵌套类型的默认可访问性是private而不是internal。
- 从外层类以外访问嵌套类型,需要使用外层类名称进行限定(就像访问静态成员一样)。
嵌套类型的访问
Test.Test1 t = new Test.Test1();
如果使用嵌套类型的主要原因是为了避免命名空间中类型定义杂乱无章,那么可以考虑使用嵌套命名空间。使用嵌套类型的原因应当是利用它较强的访问控制能力,或者是因为嵌套的类型必须访问外层类型的私有成员。
九、泛型
C#有两种不同的机制来编写跨类型可复用的代码:继承和泛型。但继承的复用性来自基类,而泛型的复用性是通过带有“占位符”的“模板”类型实现的。和继承相比,泛型能够提高类型的安全性,并减少类型的转换和装箱。
C#的泛型和C++的模板是相似的概念,但它们的工作方法不同。
9.1、泛型类型
泛型类型中声明的类型参数(占位符类型)需要由泛型类型的提供类型参数的一方填充。
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);
int x = stack.Pop(); // x is 10
int y = stack.Pop(); // y is 5
Stack用类型参数int填充T,这会在运行时隐式创建一个类型:Stack。若试图将一个字符串加入Stack中则会产生一个编译时错误。
技术上,我们称Stack是开放类型,称Stack是封闭类型。在运行时,所有的泛型实例都是封闭的,占位符已经被类型填充。
var stack = new Stack<T> (); // 报错 因为不确定T是什么类型
只有在类或者方法的内部,T才可以定义为类型参数。
public class Stack<T>
{
...
public Stack<T> Clone()
{
Stack<T> clone = new Stack<T> (); // Legal
...
}
}
9.2、泛型方法
泛型方法在方法的签名中声明类型参数。使用泛型方法,许多基本算法就可以用通用方式实现了。以下是交换两个任意类型T的变量值的泛型方法
static void Swap<T> (ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
int x = 5;
int y = 10;
Swap (ref x, ref y);
通常调用泛型方法不需要提供类型参数,因为编译器可以隐式推断得出。如果有二义性,则可以用下面的方式调用泛型方法:
Swap<int> (ref x, ref y);
在泛型中,只有引入类型参数(用尖括号标出)的方法才可归为泛型方法。泛型Stack类中的Pop方法仅仅使用了类型中已有的类型参数T,因此不属于泛型方法。
唯有方法和类可以引入类型参数。属性、索引器、事件、字段、构造器、运算符等都不能声明类型参数,虽然它们可以参与使用所在类型中已经声明的类型参数。例如,在泛型的栈中我们可以写一个索引器返回一个泛型项。
public T this [int index] => data [index];
类似的,构造器可以参与使用已经存在的类型参数,但是不能引入新的类型参数
public Stack<T> () { } // 报错
9.3、声明类型参数
可以在声明类、结构体、接口、委托和方法时引入类型参数。其他的结构,如属性,虽不能引入类型参数,但可以使用类型参数。例如,属性Value使用T。
public struct Nullable<T>
{
public T Value { get; }
}
//泛型或方法可以有多个参数
class Dictionary<TKey, TValue> {...}
Dictionary<int, string> myDic = new Dictionary<int, string> ();
只要类型参数的数量不同,泛型类型名和泛型方法的名称就可以进行重载。
class A {}
class A<T> {}
class A<T1, T2> {}
习惯上,如果泛型类型和泛型方法只有一个类型参数,且参数的含义明确,那么一般将其命名为T。当使用多个类型参数时,每一个类型参数都使用T作为前缀,后面跟一个更具描述性的名称。
9.4、typeof和未绑定泛型类型
在运行时不存在开放的泛型类型:开放泛型类型将汇编为程序的一部分而封闭。但运行时可能存在未绑定(unbound)的泛型类型,只作为Type对象存在。C#中唯一指定未绑定泛型类型的方式是使用typeof运算符
class A<T> { }
Type t = typeof(A<>);
//也可以指定一个具体类型
Type t1 = typeof (A<int> );
Console.WriteLine(t);
9.5、泛型的默认值
default关键字可用于获取泛型类型参数的默认值。引用类型的默认值为null,而值类型的默认值是将值类型的所有字段按位设置为0的值。
class A
{
public static void select<T>()
{
Console.WriteLine(default (T));
}
}
A.select<int>();//0 int类型默认值
9.6、泛型的约束
默认情况下,类型参数可以由任何类型来替换。在类型参数上应用约束,可以将类型参数定义为指定的类型参数。以下列出了可用的约束:
where T : base-class // Base-class constraint
where T : interface // Interface constraint
where T : class // Reference-type constraint
where T : struct // Value-type constraint (excludes Nullable types)
where T : new() // Parameterless constructor constraint
where U : T // Naked type constraint
在下面的例子中GenericClass<T, U>的T要求派生自(或者本身就是)SomeClass并且实现Interface1;要求U提供无参数构造器。
class SomeClass {}
interface Interface1 {}
class GenericClass<T, U> where T : SomeClass, Interface1
where U : new()
{...}
基类约束要求类型参数必须是子类(或者匹配基类);接口约束要求类型参数必须实现特定的接口。这些约束要求类型参数的实例可以隐式转换为相应的类和接口。
类约束和结构体约束规定T必须是引用类型或值类型(不能为空)。结构体约束的一个很好的例子是System.Nullable结构体。
struct Nullable<T> where T : struct {...}
无参数构造器约束要求T有一个无参数构造器。如果定义了这个约束,就可以在T上调用new()了。
static void Initialize<T> (T[] array) where T : new()
{
for (int i = 0; i < array.Length; i++)
array[i] = new T();
}
裸类型约束要求一个类型参数必须从另一个类型参数中派生(或匹配)。本例中,FilteredStack方法返回了另一个Stack,返回的Stack仅包含原来类中的一部分元素,并且类型参数U是类型参数T的子类。
class Father
{
public virtual void eat()
{
Console.WriteLine("爸爸吃");
}
}
class Child :Father
{
public override void eat()
{
Console.WriteLine("儿子吃");
}
}
class A<T>
{
public static A<U> select<U> ()where U:T
{
return new A<U>();
}
}
A<Father>.select<Child>();//只能是父子类或自己
A<Child>.select<Child>();
9.7、继承泛型类型
泛型类和非泛型类一样都可以派生子类。并且子类中仍可以令基类中类型参数保持开放。
class Stack<T> {...}
class SpecialStack<T> : Stack<T> {...}
//子类也可以用具体的类型来封闭泛型参数
class IntStack : Stack<int> {...}
class IntStack : Stack<T> {...}//编译错误
//子类型还可以引入新的类型参数
class List<T> {...}
class KeyedList<T, TKey> : List<T> {...}
技术上,子类型中所有的类型参数都是新的:可以说子类型封闭后又重新开放了基类的类型参数。这表明子类可以为其重新打开的类型参数使用更有意义的新名称。
class List<T> {...}
class KeyedList<TElement, TKey> : List<TElement> {...}
9.8、自引用泛型声明
一个类型可以使用自身类型作为具体类型来封闭类型参数
public interface IEquatable<T> { bool Equals (T obj); }
public class Balloon : IEquatable<Balloon>
{
public string Color { get; set; }
public int CC { get; set; }
public bool Equals (Balloon b)
{
if (b == null) return false;
return b.Color == Color && b.CC == CC;
}
}
class Foo<T> where T : IComparable<T> { ... }
class Bar<T> where T : Bar<T> { ... }
9.9、静态数据
静态数据对于每一个封闭的类型来说都是唯一的。
class Bob<T> { public static int Count; }
class Test
{
static void Main()
{
Console.WriteLine (++Bob<int> .Count); // 1
Console.WriteLine (++Bob<int> .Count); // 2
Console.WriteLine (++Bob<string> .Count); // 1 类型换了就只属当前类型
Console.WriteLine (++Bob<object> .Count); // 1
}
}
9.10、类型参数的转换
C#的类型转换运算符可以进行多种的类型转换:
- 数值转换
- 引用转换
- 装箱/拆箱转换
- 自定义转换(通过运算符重载)
根据已知操作数的类型,在编译时就已经决定了类型转换的方式。但在编译时操作数的类型还并未确定,使得上述规则在泛型类型参数上会出现特殊的情形。如果导致了二义性,那么编译器会产生一个错误。
StringBuilder Foo<T> (T arg)
{
if (arg is StringBuilder)
return (StringBuilder) arg; // 此处不知道T是什么类型,能不能转StringBuilder还未可知,会报错
...
}
StringBuilder Foo<T> (T arg)
{
StringBuilder sb = arg as StringBuilder;
if (sb ! = null) return sb;
...
}
而更一般的做法是先将其转换为object类型。这种方法行得通,因为从object转换,或将对象转换为object都不是自定义转换,而是引用或者装箱/拆箱转换。
return (StringBuilder) (object) arg;//这样做也有问题,不同类型转会报错
9.11、协变
假定A可以转换为B,如果A可以转换为B那么称X有一个协变类型参数。
由于C#有协变(covariance)和逆变(contravariance)的概念,所以“可转换”意味着可以通过隐式引用转换进行类型转换,例如,A是B的子类或者A实现B。而数值转换、装箱转换和自定义转换是不包含在内的。
IFoo<string> s = ...;
IFoo<object> b = s;
从C# 4.0开始,泛型接口就支持协变类型参数了(泛型委托也支持协变类型参数,请参见第4章),但是泛型类是不支持的。数组也支持协变(如果A有一个隐式引用转换为B,则A[]可以转换为B[])。
协变和逆变(或简称可变性)都是高级概念。在C#中引入和强化协变的动机在于允许泛型接口和泛型类型(尤其是框架定义的那些类型,例如IEnumerable)像人们期待的那样工作。即使你不了解它们背后的细节,也可以从中获益。
可变性不是自动的
为了保证静态类的安全性,泛型类型参数不是自动协变的。
class Animal {}
class Bear : Animal {}
class Camel : Animal {}
public class Stack<T> // A simple Stack implementation
{
int position;
T[] data = new T[100];
public void Push (T obj) => data[position++] = obj;
public T Pop() => data[--position];
}
Stack<Bear> bears = new Stack<Bear> ();
Stack<Animal> animals = bears; //编译错误
这种约束避免了以下代码可能产生的运行时错误
animals.Push (new Camel());