C#2.0
泛型
泛型将类型参数的概念引入 .NET Framework
,这样就可以设计具有以下特征的类和方法:在客户端代码声明并初始化这些类或方法之前,这些类或方法会延迟指定一个或多个类型。 例如,通过使用泛型类型参数 T
,可以编写其他客户端代码能够使用的单个类,而不会产生运行时转换或装箱操作的成本或风险。
// Declare the generic class.
public class GenericList<T>
{
public void Add(T input) { }
}
class TestGenericList
{
private class ExampleClass { }
static void Main()
{
// Declare a list of type int.
GenericList<int> list1 = new GenericList<int>();
list1.Add(1);
// Declare a list of type string.
GenericList<string> list2 = new GenericList<string>();
list2.Add("");
// Declare a list of type ExampleClass.
GenericList<ExampleClass> list3 = new GenericList<ExampleClass>();
list3.Add(new ExampleClass());
}
}
泛型类和泛型方法兼具可重用性、类型安全性和效率,这是非泛型类和非泛型方法无法实现的。 泛型通常与集合以及作用于集合的方法一起使用。 System.Collections.Generic
命名空间包含几个基于泛型的集合类。 非泛型集合(如 ArrayList
)不建议使用,并且保留用于兼容性目的。
泛型概述
- 使用泛型类型可以最大限度地重用代码、保护类型安全性以及提高性能。
- 泛型最常见的用途是创建集合类。
.NET Framework
类库在System.Collections.Generic
命名空间中包含几个新的泛型集合类。 应尽可能使用这些类来代替某些类,如 使用List<T>
来代替System.Collections
命名空间中的ArrayList
。- 可以创建自己的泛型接口、泛型类、泛型方法、泛型事件和泛型委托。
- 可以对泛型类进行约束以访问特定数据类型的方法。
- 在泛型数据类型中所用类型的信息可在运行时通过使用反射来获取。
泛型类型参数
在泛型类型或方法定义中,类型参数是在创建泛型类型的一个实例时,客户端指定的特定类型的占位符。在泛型声明中,类型参数要放在一对尖括号内,如果有多个类型参数则以逗号分隔。
具有泛型类型参数的类型称为开放类型,CLR禁止构造开放类型的任何实例。
为所有类型参数都传递了实际的数据类型后,类型就成了封闭类型。CLR允许构造封闭类型的实例。
泛型类型参数的命名:
- 单个类型参数,一般命名为
T
. - 多个类型参数,一般命名为
T+类型描述
.
public class List<T> { /*...*/ }
public delegate TOutput Converter<TInput, TOutput>(TInput from);
public interface ISessionChannel<TSession>
{
TSession Session { get; }
}
泛型类型参数的约束
约束是来告知编译器,类型参数必须具备的功能。在没有任何约束的情况下,类型参数可以是任何类型。
- 引用类型约束
where T : class
- 值类型约束
where T : struct
- 构造函数类型约束
where T : new()
- 转换类型约束
where T : 基类|接口|类型参数
- 非托管类型约束(C#7.3)
where T : unmanaged
- 委托约束(C#7.3)
where T : System.Delegate
- 枚举约束(C#7.3)
where T : System.Enum
- 非空约束(C#8.0)
where T : notnull
泛型代码爆炸
使用泛型类型参数的方法在进行JIT编译时,CLR获取方法的IL,用指定的类型实参替换,然后创建恰当的本机代码。单这样做有个缺点:CLR要为每种不同的方法/类型组合生成本机代码。我们将这个现象称为代码爆炸。它可能造成应用程序的工作集显著增大,从而损害性能。
CLR内建了一些优化措施能缓解代码爆炸:
- 假如为特定的类型实参调用了一个方法,以后再用相同的类型实参调用这个方法,CLR只会为这个方法/类型组合编译一次代码。
- CLR认为所有的引用类型实参都完全相同,所以代码能够共享(因为所有引用类型的实参或变量实际上只是指向堆上对象的指针)。而值类型就不可以,因为每个值类型的大小不定,即使两个值类型的大小一样(比如Int32和UInt32),CLR仍然无法共享代码,因为可能要用不同的本机CPU指令来操纵这些值。
分部类型
partial
关键字指示可在命名空间中定义类、结构或接口的其他部分。所有部分都必须使用 partial
关键字。 在编译时,各个部分都必须可用来形成最终的类型。 各个部分必须具有相同的可访问性,如 public
、private
等。
partial
修饰符不可用于委托或枚举声明中。
如果将任意部分声明为抽象的,则整个类型都被视为抽象的。 如果将任意部分声明为密封的,则整个类型都被视为密封的。 如果任意部分声明基类型,则整个类型都将继承该类。
分部类型功能完全由C#编译器实现(编译时对分部类型定义的成员进行合并),CLR对该功能一无所知,这就解释了一个类型的所有源代码文件为什么必须使用相同编程语言,而且必须作为一个编译单元编译到一起。
public partial class Employee
{
public void DoWork()
{
}
}
public partial class Employee
{
public void GoToLunch()
{
}
}
匿名方法
匿名方法(Anonymous methods
) 提供了一种传递代码块作为委托参数的技术。匿名方法是没有名称只有主体的方法。
在匿名方法中您不需要指定返回类型,它是从方法主体内的 return 语句推断的。
delegate
运算符创建一个可以转换为委托类型的匿名方法.
delegate void MyDelegate(int n);
MyDelegate nc = delegate(int x)
{
Console.WriteLine("Anonymous Method: {0}", x);
};
可空值类型
我们知道值类型的变量永远不会为null:它总是包含值类型的值本身。事实上,这正是“值类型”一词的由来。遗憾的是,这在某些情况下会成为问题。例如数据库可将一个列的数据类型定义成一个32位证书,并映射到FCL的Int32。但是数据库中的一个列可能允许值为空,但是在CLR中没有办法将Int32值表示成null。
为了解决这个问题,Microsoft在CLR中引入了可空值类型的概念:System.Nullable<T>
任何可为空的值类型都是泛型 System.Nullable<T>
结构的实例。 可使用以下任何一种可互换形式引用具有基础类型 T
的可为空值类型:Nullable<T>
或 T?
。
可空值类型的装箱与拆箱
当CLR对 Nullable<T>
实例进行装箱时,会检查它是否为null
。如果是,CLR不装箱任何东西,直接返回null
。如果可空实例不为null,CLR从可空实例中取出值并进行装箱。
int? a=null;
object o=a;//不装箱
a=1;
o=a;//o引用一个已装箱的Int32值
当对已装箱的值类型实例进行拆箱时,会检查它是否为null
。如果是,CLR将Nullable<T>
的值设为null
。如果不为null,CLR将对实例值进行拆箱。
object o=5;
int? a=(int?)o;// a=5
int b=(int)o;// b=5
o=null;
int? a=(int?)o;// a=null;
int b=(int)o;// NullReferenceException
判断是否是可空类型
在Nullable<T>
对象上调用GetType,CLR实际会“撒谎”说类型是T
,而不是Nullable<T>
。
如果要确定实例是否是可为空的值类型,请使用Nullable.GetUnderlyingType(type) != null
。
bool IsOfNullableType<T>(T o)
{
var type = typeof(T);
return Nullable.GetUnderlyingType(type) != null;
}
迭代器
在.NET中,迭代器是通过 IEnumerator
和 IEnumerable
接口及他们的泛型等价物来封装的。如果某个类型实现了 IEnumerable
接口,就意味着它可以被迭代访问。调用 GetEnumerator
方法将返回 IEnumerator
的实现,这就是迭代器本身。
迭代器 可用于逐步迭代集合,例如列表和数组。
迭代器方法或 get
访问器可对集合执行自定义迭代。 迭代器方法使用 yield return
语句返回元素,每次返回一个。 到达 yield return
语句时,会记住当前在代码中的位置。 下次调用迭代器函数时,将从该位置重新开始执行。
通过 foreach
语句或 LINQ
查询从客户端代码中使用迭代器。
//该实例打印了斐波那契数列的前5个值
static void Main(string[] args)
{
IEnumerable<int> collection = Fibonacci().Take(5);
foreach (var item in collection)
{
Console.WriteLine(item);
}
}
private static IEnumerable<int> Fibonacci()
{
int current = 1, next = 1;
while (true)
{
yield return current;
next = current + (current = next);
}
}
协变和逆变
在 C# 中,协变和逆变能够实现数组类型、委托类型和泛型类型参数的隐式引用转换。 协变保留分配兼容性,逆变则与之相反。
逆变量(contravariant
):意味着可以从一个类更改为它的某个派生类。C# 用 in
关键字来标记逆变量。逆变量只能出现在输入位置,比如方法的参数。
协变量(convariant
):意味着可以从一个类更改为它的某个基类。C# 用 out
关键字来标记协变量。协变量只能出现在输出位置,比如方法的返回。
以下代码演示分配兼容性、协变和逆变之间的差异。
// 分配兼容性
string str = "test";
object obj = str;
// 协变
IEnumerable<string> strings = new List<string>();
// An object that is instantiated with a more derived type argument
// is assigned to an object instantiated with a less derived type argument.
// Assignment compatibility is preserved.
IEnumerable<object> objects = strings;
// 假定以下方法在类中:
// static void SetObject(object o) { }
// 逆变
Action<object> actObject = SetObject;
Action<string> actString = actObject;
静态类
静态类基本上与非静态类相同,但存在一个差异:静态类无法实例化。 换句话说,无法使用 new 运算符创建类类型的变量。 由于不存在任何实例变量,因此可以使用类名本身访问静态类的成员。
编译器实际上将静态类声明为abstract sealed
,所以静态类不能被继承,不能实例化。
与所有类类型的情况一样,加载引用静态类的程序时,.NET Framework
公共语言运行时 (CLR) 会加载静态类的类型信息。 程序无法确切指定类加载的时间。 但是,可保证进行加载,以及在程序中首次引用静态类之前初始化其静态字段并调用其静态构造函数。 静态构造函数只调用一次,在程序所驻留的应用程序域的生存期内,静态类会保留在内存中。
public static class MyStaticClass
{
public static void Hello(string name)
{
Console.WriteLine("hello {0}~", name);
}
}