学习c#的第二十一天
目录
C# 泛型(Generic)
泛型类型参数
类型参数的约束
约束多个参数
未绑定的类型参数
类型参数作为约束
notnull 约束
class 约束
default 约束
非托管约束
委托约束
枚举约束
类型参数实现声明的接口
泛型类
泛型方法
泛型和数组
泛型
数组
泛型和数组的结合运用
泛型委托
委托
泛型委托
C++ 模板和 C# 泛型之间的区别
C# 泛型(Generic)
泛型类型参数
在泛型类型或方法定义中,类型参数充当了一个占位符,用于在创建泛型类型的实例时由客户端指定特定的类型。 泛型类(例如泛型介绍中列出的 GenericList<T>)本身并不是一个具体的类型,而更像是类型的模板或蓝图。要使用 GenericList<T>,客户端代码必须在尖括号内指定类型参数,以声明并实例化特定的构造类型。这个类型参数可以是编译器能够识别的任何类型。通过这种方式,可以创建任意数量的泛型类型实例,每个实例都使用不同的类型参数。
举个例子,假设有一个泛型类 GenericList<T>,客户端可以这样使用它来创建不同类型的实例:
GenericList<int> intList = new GenericList<int>();
GenericList<string> stringList = new GenericList<string>();
GenericList<Customer> customerList = new GenericList<Customer>();
在这个例子中,我们使用了三种不同的类型参数(int、string 和 Customer),分别实例化了三个不同类型的 GenericList。每个实例在运行时会被视为独立的类型,它们拥有各自特定的类型参数,并且可以确保类型安全性和有效性。
类型参数的约束
约束告知编译器类型参数必须具备的功能。 在没有任何约束的情况下,类型参数可以是任何类型。 编译器只能假定 System.Object 的成员,它是任何 .NET 类型的最终基类。 如果客户端代码使用不满足约束的类型,编译器将发出错误。 通过使用 where 上下文关键字指定约束。 下表列出了各种类型的约束:
约束 | 描述 |
---|---|
where T : struct | 类型参数必须是不可为 null 的值类型。 有关可为 null 的值类型的信息,请参阅可为 null 的值类型。 由于所有值类型都具有可访问的无参数构造函数,因此 struct 约束表示 new() 约束,并且不能与 new() 约束结合使用。 struct 约束也不能与 unmanaged 约束结合使用。 |
where T : class | 类型参数必须是引用类型。 此约束还应用于任何类、接口、委托或数组类型。 在可为 null 的上下文中,T 必须是不可为 null 的引用类型。 |
where T : class? | 类型参数必须是可为 null 或不可为 null 的引用类型。 此约束还应用于任何类、接口、委托或数组类型。 |
where T : notnull | 类型参数必须是不可为 null 的类型。 参数可以是不可为 null 的引用类型,也可以是不可为 null 的值类型。 |
where T : default | 重写方法或提供显式接口实现时,如果需要指定不受约束的类型参数,此约束可解决歧义。 default 约束表示基方法,但不包含 class 或 struct 约束。 有关详细信息,请参阅default约束规范建议。 |
where T : unmanaged | 类型参数必须是不可为 null 的非托管类型。 unmanaged 约束表示 struct 约束,且不能与 struct 约束或 new() 约束结合使用。 |
where T : new() | 类型参数必须具有公共无参数构造函数。 与其他约束一起使用时,new() 约束必须最后指定。 new() 约束不能与 struct 和 unmanaged 约束结合使用。 |
where T : <基类名> | 类型参数必须是指定的基类或派生自指定的基类。 在可为 null 的上下文中,T 必须是从指定基类派生的不可为 null 的引用类型。 |
where T : <基类名>? | 类型参数必须是指定的基类或派生自指定的基类。 在可为 null 的上下文中,T 可以是从指定基类派生的可为 null 或不可为 null 的类型。 |
where T : <接口名称> | 类型参数必须是指定的接口或实现指定的接口。 可指定多个接口约束。 约束接口也可以是泛型。 在的可为 null 的上下文中,T 必须是实现指定接口的不可为 null 的类型。 |
where T : <接口名称>? | 类型参数必须是指定的接口或实现指定的接口。 可指定多个接口约束。 约束接口也可以是泛型。 在可为 null 的上下文中,T 可以是可为 null 的引用类型、不可为 null 的引用类型或值类型。 T 不能是可为 null 的值类型。 |
where T : U | 为 T 提供的类型参数必须是为 U 提供的参数或派生自为 U 提供的参数。 在可为 null 的上下文中,如果 U 是不可为 null 的引用类型,T 必须是不可为 null 的引用类型。 如果 U 是可为 null 的引用类型,则 T 可以是可为 null 的引用类型,也可以是不可为 null 的引用类型。 |
使用约束的原因
约束指定类型参数的功能和预期。 声明这些约束意味着你可以使用约束类型的操作和方法调用。 如果泛型类或方法对泛型成员使用除简单赋值之外的任何操作或调用 System.Object 不支持的任何方法,则将对类型参数应用约束。 例如,基类约束告诉编译器,仅此类型的对象或派生自此类型的对象可用作类型参数。 编译器有了此保证后,就能够允许在泛型类中调用该类型的方法。 以下代码示例演示可通过应用基类约束添加到(泛型介绍中的)GenericList<T>
类的功能。
public class Employee
{
public Employee(string name, int id) => (Name, ID) = (name, id);
public string Name { get; set; }
public int ID { get; set; }
}
public class GenericList<T> where T : Employee
{
private class Node
{
public Node(T t) => (Next, Data) = (null, t);
public Node? Next { get; set; }
public T Data { get; set; }
}
private Node? head;
public void AddHead(T t)
{
Node n = new Node(t) { Next = head };
head = n;
}
public IEnumerator<T> GetEnumerator()
{
Node? current = head;
while (current != null)
{
yield return current.Data;
current = current.Next;
}
}
public T? FindFirstOccurrence(string s)
{
Node? current = head;
T? t = null;
while (current != null)
{
//The constraint enables access to the Name property.
if (current.Data.Name == s)
{
t = current.Data;
break;
}
else
{
current = current.Next;
}
}
return t;
}
}
约束使泛型类能够使用 Employee.Name 属性。 约束指定类型 T 的所有项都保证是 Employee 对象或从 Employee 继承的对象。
可以对同一类型参数应用多个约束,并且约束自身可以是泛型类型,如下所示:
class EmployeeList<T> where T : Employee, IEmployee, System.IComparable<T>, new()
{
// ...
}
在应用 where T : class
约束时,请避免对类型参数使用 ==
和 !=
运算符,因为这些运算符仅测试引用标识而不测试值相等性。 即使在用作参数的类型中重载这些运算符也会发生此行为。 下面的代码说明了这一点;即使 String 类重载 ==
运算符,输出也为 false。
public static void OpEqualsTest<T>(T s, T t) where T : class
{
System.Console.WriteLine(s == t);
}
private static void TestStringEquality()
{
string s1 = "target";
System.Text.StringBuilder sb = new System.Text.StringBuilder("target");
string s2 = sb.ToString();
OpEqualsTest<string>(s1, s2);
}
编译器只知道 T 在编译时是引用类型,并且必须使用对所有引用类型都有效的默认运算符。 如果必须测试值相等性,建议同时应用 where T : IEquatable<T> 或 where T : IComparable<T> 约束,并在用于构造泛型类的任何类中实现该接口。
约束多个参数
可以对多个参数应用多个约束,对一个参数应用多个约束,如下例所示:
class Base { }
class Test<T, U>
where U : struct
where T : Base, new()
{ }
未绑定的类型参数
没有约束的类型参数(如公共类 SampleClass<T>{}
中的 T)称为未绑定的类型参数。 未绑定的类型参数具有以下规则:
- 不能使用
!=
和==
运算符,因为无法保证具体的类型参数能支持这些运算符。 - 可以在它们与
System.Object
之间来回转换,或将它们显式转换为任何接口类型。 - 可以将它们与 null 进行比较。 将未绑定的参数与
null
进行比较时,如果类型参数为值类型,则该比较将始终返回 false。
类型参数作为约束
在具有自己类型参数的成员函数必须将该参数约束为包含类型的类型参数时,将泛型类型参数用作约束非常有用,如下例所示:
public class List<T>
{
public void Add<U>(List<U> items) where U : T {/*...*/}
}
在上述示例中,T 在 Add 方法的上下文中是一个类型约束,而在 List 类的上下文中是一个未绑定的类型参数。
类型参数还可在泛型类定义中用作约束。 必须在尖括号中声明该类型参数以及任何其他类型参数:
//Type parameter V is used as a type constraint.
public class SampleClass<T, U, V> where T : V { }
类型参数作为泛型类的约束的作用非常有限,因为编译器除了假设类型参数派生自 System.Object 以外,不会做其他任何假设。 如果要在两个类型参数之间强制继承关系,可以将类型参数用作泛型类的约束。
notnull 约束
可以使用 notnull 约束指定类型参数必须是不可为 null 的值类型或不可为 null 的引用类型。 与大多数其他约束不同,如果类型参数违反 notnull 约束,编译器会生成警告而不是错误。
notnull 约束仅在可为 null 上下文中使用时才有效。 如果在过时的可为 null 上下文中添加 notnull 约束,编译器不会针对违反约束的情况生成任何警告或错误。
class 约束
可为 null 的上下文中的 class 约束指定类型参数必须是不可为 null 的引用类型。 在可为 null 上下文中,当类型参数是可为 null 的引用类型时,编译器会生成警告。
default 约束
添加可为空引用类型会使泛型类型或方法中的 T? 使用复杂化。 T? 可以与 struct 或 class 约束一起使用,但必须存在其中一项。 使用 class 约束时,T? 引用了 T 的可为空引用类型。 从 C# 9 开始,可在这两个约束均未应用时使用 T?。 在这种情况下,对于值类型和引用类型,T? 解读为 T?。 但是,如果 T是 Nullable<T>的实例,则 T? 与 T 相同。 换句话说,它不会成为 T??。
由于现在可在没有 class 或 struct 约束的情况下使用 T?,因此在重写或显式接口实现中可能会出现歧义。 在这两种情况下,重写不包含约束,但从基类继承。 当基类不应用 class 或 struct 约束时,派生类需要通过某种方式在不使用任一种约束的情况下指定应用于基方法的重写。 此时派生方法将应用 default 约束。 default 约束不阐明 class 和 struct 约束。
非托管约束
可使用 unmanaged 约束来指定类型参数必须是不可为 null 的非托管类型。通过 unmanaged 约束,用户能编写可重用例程,从而使用可作为内存块操作的类型,如以下示例所示:
unsafe public static byte[] ToByteArray<T>(this T argument) where T : unmanaged
{
var size = sizeof(T);
var result = new Byte[size];
Byte* p = (byte*)&argument;
for (var i = 0; i < size; i++)
result[i] = *p++;
return result;
}
以上方法必须在 unsafe 上下文中编译,因为它并不是在已知的内置类型上使用 sizeof 运算符。 如果没有 unmanaged 约束,则 sizeof 运算符不可用。
unmanaged 约束表示 struct 约束,且不能与其结合使用。 因为 struct 约束表示 new() 约束,且 unmanaged 约束也不能与 new() 约束结合使用。
委托约束
可以使用 System.Delegate 或 System.MulticastDelegate 作为基类约束。 CLR 始终允许此约束,但 C# 语言不允许。 使用 System.Delegate 约束,用户能够以类型安全的方式编写使用委托的代码。 以下代码定义了合并两个同类型委托的扩展方法:
public static TDelegate? TypeSafeCombine<TDelegate>(this TDelegate source, TDelegate target)
where TDelegate : System.Delegate
=> Delegate.Combine(source, target) as TDelegate;
可使用上述方法来合并相同类型的委托:
Action first = () => Console.WriteLine("this");
Action second = () => Console.WriteLine("that");
var combined = first.TypeSafeCombine(second);
combined!();
Func<bool> test = () => true;
// Combine signature ensures combined delegates must
// have the same type.
//var badCombined = first.TypeSafeCombine(test);
如果取消评论最后一行,它将不会编译。 first 和 test 均为委托类型,但它们是不同的委托类型。
枚举约束
还可指定 System.Enum 类型作为基类约束。 CLR 始终允许此约束,但 C# 语言不允许。使用 System.Enum 的泛型提供类型安全的编程,缓存使用 System.Enum 中静态方法的结果。 以下示例查找枚举类型的所有有效的值,然后生成将这些值映射到其字符串表示形式的字典。
public static Dictionary<int, string> EnumNamedValues<T>() where T : System.Enum
{
var result = new Dictionary<int, string>();
var values = Enum.GetValues(typeof(T));
foreach (int item in values)
result.Add(item, Enum.GetName(typeof(T), item)!);
return result;
}
Enum.GetValues 和 Enum.GetName 使用反射,这会对性能产生影响。 可调用 EnumNamedValues 来生成可缓存和重用的集合,而不是重复执行需要反射才能实施的调用。
如以下示例所示,可使用它来创建枚举并生成其值和名称的字典:
enum Rainbow
{
Red,
Orange,
Yellow,
Green,
Blue,
Indigo,
Violet
}
var map = EnumNamedValues<Rainbow>();
foreach (var pair in map)
Console.WriteLine($"{pair.Key}:\t{pair.Value}");
类型参数实现声明的接口
某些场景要求为类型参数提供的参数实现该接口。 例如:
public interface IAdditionSubtraction<T> where T : IAdditionSubtraction<T>
{
public abstract static T operator +(T left, T right);
public abstract static T operator -(T left, T right);
}
此模式使 C# 编译器能够确定重载运算符或任何 static virtual 或 static abstract 方法的包含类型。 它提供的语法使得可以在包含类型上定义加法和减法运算符。 如果没有此约束,需要将参数和自变量声明为接口,而不是类型参数:
public interface IAdditionSubtraction<T> where T : IAdditionSubtraction<T>
{
public abstract static IAdditionSubtraction<T> operator +(
IAdditionSubtraction<T> left,
IAdditionSubtraction<T> right);
public abstract static IAdditionSubtraction<T> operator -(
IAdditionSubtraction<T> left,
IAdditionSubtraction<T> right);
}
上述语法要求实现者对这些方法使用显式接口实现。 提供额外的约束使接口能够根据类型参数来定义运算符。 实现接口的类型可以隐式实现接口方法。
泛型类
泛型类是一种具有泛型类型参数的类,它可以在定义时不指定具体的数据类型,而在实例化时再指定具体的数据类型。使用泛型类可以编写出更加通用和灵活的代码,以适应各种不同类型的数据。
1、泛型类的定义:泛型类的定义与普通类类似,只是在类名后面使用尖括号加上类型参数,例如:public class MyGenericClass<T> { /*...*/ }。
2、类型参数 T:类型参数 T 是一个占位符,它代表着实际的数据类型,在实例化时将会被替换为具体的类型。可以有多个类型参数,用逗号分隔。
3、使用类型参数:在泛型类的定义中,可以在类的字段、属性、方法等地方使用类型参数 T,从而创建与特定类型无关的通用代码。要将何种约束(如有)应用到类型参数(请参阅类型参数的约束)。
4、实例化泛型类:在实例化泛型类时,需要为类型参数 T 指定具体的数据类型,例如:MyGenericClass<int> myObj = new MyGenericClass<int>();,这样就创建了一个具体类型为 int 的泛型类实例。
5、泛型类的优势:
- 提高代码的复用性:通过泛型类,可以编写出可以适用于多种数据类型的通用代码,提高代码的复用性。
- 增强类型安全性:泛型类可以在编译时捕获一些类型不匹配或错误使用,增强代码的类型安全性。
- 提高性能:泛型类可以避免装箱和拆箱操作,提高程序的性能。
6、标准.NET泛型类:在.NET框架中,有许多标准的泛型类,如 List<T>、Dictionary<TKey, TValue> 等,它们提供了对泛型编程的丰富支持。 有关使用这些类的详细信息,请参阅 .NET 中的泛型集合。
7、实现一个泛型接口还是多个泛型接口:例如,如果要设计用于在基于泛型的集合中创建项的类,则可能必须实现一个接口,例如 IComparable<T>,其中 T
为类的类型。
总之,泛型类是一种非常有用的工具,可以帮助我们编写出更加通用、灵活和健壮的代码,提高代码的复用性和可维护性。
以下是一个代码示例:
using System;
using System.Collections;
using System.Collections.Generic;
// 创建泛型队列类并实现泛型接口 IEnumerable<T>
public class Queue<T> : IEnumerable<T>
{
private List<T> items; // 使用 List<T> 存储队列中的元素
// 构造函数,初始化队列
public Queue()
{
items = new List<T>();
}
// 将元素添加到队列末尾
public void Enqueue(T item)
{
items.Add(item);
}
// 从队列头部移除并返回元素
public T Dequeue()
{
if (items.Count == 0)
{
throw new InvalidOperationException("The queue is empty");
}
T item = items[0];
items.RemoveAt(0);
return item;
}
// 实现 IEnumerable<T> 接口的 GetEnumerator 方法
public IEnumerator<T> GetEnumerator()
{
foreach (T item in items)
{
yield return item;
}
}
// 实现 IEnumerable 接口的 GetEnumerator 方法
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
class Program
{
static void Main()
{
Queue<int> intQueue = new Queue<int>(); // 创建存储整数的队列
intQueue.Enqueue(10); // 添加元素
intQueue.Enqueue(20);
// 使用 foreach 循环对队列中的元素进行迭代输出
foreach (int item in intQueue)
{
Console.WriteLine(item);
}
}
}
在这个示例中,我们创建了一个名为 Queue 的泛型类,实现了泛型接口 IEnumerable<T>。在 Queue 类中,我们使用了 List<T> 来存储队列中的元素,并提供了 Enqueue 和 Dequeue 方法来向队列中添加和移除元素。通过实现 IEnumerable<T> 接口,我们使得 Queue 类可以被用于 foreach 循环,从而对队列中的元素进行迭代输出。
在 Main 方法中,我们创建了一个存储整数的队列 intQueue,并向其中添加了两个整数。随后,我们使用 foreach 循环对队列中的元素进行迭代输出。
通过这个示例,我们展示了如何创建一个泛型类,并实现一个泛型接口以提供迭代功能。这种方式可以使得我们的泛型类更加灵活和通用。
泛型方法
泛型方法是一种在方法中使用泛型类型参数的技术,它允许我们编写能够处理多种类型数据的方法,而不需要为每种类型都编写单独的方法。在C#中,我们可以使用泛型方法来实现这一点。
以下是泛型方法的特点和用法:
- 灵活性: 泛型方法可以处理各种类型的数据,例如整数、浮点数、字符串等,而不需要针对每种类型编写单独的方法。
- 代码重用: 泛型方法提高了代码的重用性,因为一个泛型方法可以适用于多种数据类型,避免了重复编写类似的方法。
- 类型安全: 使用泛型方法可以在编译时进行类型检查,确保方法在处理数据时符合类型约束。
下面是一个简单的示例,演示了如何创建和使用泛型方法:
using System;
public class Program
{
// 定义一个泛型方法 Swap,用于交换两个变量的值
public static void Swap<T>(ref T a, ref T b)
{
T temp = a;
a = b;
b = temp;
}
public static void Main()
{
int x = 10, y = 20;
Console.WriteLine($"Before swap: x = {x}, y = {y}");
// 调用泛型方法 Swap 来交换整数变量的值
Swap<int>(ref x, ref y);
Console.WriteLine($"After swap: x = {x}, y = {y}");
string str1 = "Hello", str2 = "World";
Console.WriteLine($"Before swap: str1 = {str1}, str2 = {str2}");
// 调用泛型方法 Swap 来交换字符串变量的值
Swap<string>(ref str1, ref str2);
Console.WriteLine($"After swap: str1 = {str1}, str2 = {str2}");
}
}
在上面的示例中,我们定义了一个名为 Swap 的泛型方法。该方法使用了一个泛型类型参数 T,该参数可以代表任意类型。在方法体内部,我们可以像操作普通变量一样操作类型为 T 的变量。通过在方法名称后面加上尖括号和类型参数,我们可以告诉编译器这是一个泛型方法,并且在调用该方法时需要指定具体的类型。
在 Main 方法中,我们展示了如何使用泛型方法 Swap 来交换整数和字符串变量的值。在调用泛型方法时,我们需要在方法名后面的尖括号中指定具体的类型,以告诉编译器我们要使用该方法处理哪种类型的数据。
通过泛型方法,我们可以编写更加通用和灵活的代码,而无需针对不同类型重复编写多个相似的方法。这提高了代码的重用性和可维护性。
泛型和数组
泛型和数组是C#中两个非常重要的概念,它们可以结合在一起提供更强大和灵活的编程功能。
泛型
泛型是C#中的一种编程机制,它允许我们编写能够处理各种类型数据的代码,而不需要针对每种类型都编写单独的代码。通过泛型,我们可以实现代码的重用和类型安全。
泛型的特点:
灵活性: 泛型允许我们编写能够处理各种类型数据的代码,例如集合类、方法等。
类型安全: 使用泛型可以在编译时进行类型检查,确保代码在处理数据时符合类型约束。
代码重用: 泛型提高了代码的重用性,因为一个泛型类或方法可以适用于多种数据类型,避免了重复编写类似的代码。
数组
数组是一种存储相同类型元素的连续内存空间的数据结构,它是C#中最基本的数据结构之一。通过数组,我们可以方便地存储和访问多个相同类型的元素。
数组的特点:
连续存储: 数组中的元素在内存中是连续存储的,这使得访问数组中的元素非常高效。
固定长度: 数组一旦创建后,其长度通常是固定的,不能动态改变。
下标访问: 我们可以使用下标来访问数组中的元素,下标从0开始计数。
泛型和数组的结合运用
在C#中,我们可以使用泛型来创建数组,从而实现存储不同类型数据的灵活性。例如,我们可以使用泛型类 List<T> 来代替传统的数组,它可以存储任意类型的元素,并且提供了丰富的操作方法。
以下是一个简单的示例,演示了如何使用泛型类 List<T> 来存储不同类型的元素:
using System;
using System.Collections.Generic;
public class Program
{
public static void Main()
{
// 创建一个存储整数的 List
List<int> intList = new List<int>();
intList.Add(10);
intList.Add(20);
// 创建一个存储字符串的 List
List<string> stringList = new List<string>();
stringList.Add("Hello");
stringList.Add("World");
// 遍历并打印整数 List 中的元素
Console.WriteLine("Integers:");
foreach (int num in intList)
{
Console.WriteLine(num);
}
// 遍历并打印字符串 List 中的元素
Console.WriteLine("Strings:");
foreach (string str in stringList)
{
Console.WriteLine(str);
}
}
}
在上面的示例中,我们使用泛型类 List<T> 分别创建了存储整数和字符串的列表,并且成功存储和遍历了不同类型的元素。这展示了泛型和数组(通过泛型类 List<T>)结合在一起的灵活性和强大功能。
泛型委托
泛型委托是C#中的一种高级特性,它结合了泛型和委托的功能,使得我们可以定义能够处理不同类型参数的委托类型。泛型委托为我们提供了更灵活、通用的委托类型,可以在编写泛型方法或类时发挥重要作用。
委托
首先,让我们先来了解一下委托的概念。委托是一种类型安全的函数指针,它允许我们将方法作为参数传递、存储方法的引用,并且可以实现回调等功能。在C#中,委托使用 delegate 关键字进行定义。
泛型委托
泛型委托是指具有泛型参数的委托类型。通过使用泛型委托,我们可以定义一个委托类型,该委托可以接受不同类型的参数,并返回不同类型的结果。这使得我们可以编写通用的委托类型,而不需要为每种参数类型都定义一个单独的委托类型。
以下是一个简单的示例,演示了如何定义和使用泛型委托:
using System;
// 定义一个泛型委托类型
public delegate T Calculator<T>(T x, T y);
public class Program
{
// 泛型方法,接受泛型委托作为参数
public static void PerformCalculation<T>(T a, T b, Calculator<T> calculator)
{
T result = calculator(a, b);
Console.WriteLine($"Result: {result}");
}
public static void Main()
{
// 使用泛型委托进行整数加法计算
PerformCalculation(10, 20, (x, y) => x + y);
// 使用泛型委托进行字符串连接
PerformCalculation("Hello, ", "World!", (x, y) => x + y);
}
}
在上面的示例中,我们首先定义了一个泛型委托类型 Calculator<T>,它接受两个类型为 T 的参数,并返回类型为 T 的结果。然后,在 PerformCalculation 方法中,我们接受了一个泛型委托作为参数,并在方法内部使用该委托进行计算。在 Main 方法中,我们展示了如何使用泛型委托进行整数加法计算和字符串连接操作。
通过泛型委托,我们可以编写更加通用和灵活的代码,能够处理不同类型参数的计算或处理逻辑。这提高了代码的重用性和可扩展性,并使得我们的代码更具有通用性。
C++ 模板和 C# 泛型之间的区别
C# 泛型和 C++ 模板均是支持参数化类型的语言功能。 但是,两者之间存在很多不同。 在语法层次,C# 泛型是参数化类型的一个更简单的方法,而不具有 C++ 模板的复杂性。 此外,C# 不试图提供 C++ 模板所具有的所有功能。 在实现层次,主要区别在于 C# 泛型类型的替换在运行时执行,从而为实例化对象保留了泛型类型信息。有关详细信息,请参阅运行时中的泛型。
以下是 C# 泛型和 C++ 模板之间的主要差异:
- C# 泛型的灵活性与 C++ 模板不同。 例如,虽然可以调用 C# 泛型类中的用户定义的运算符,但是无法调用算术运算符。
- C# 不允许使用非类型模板参数,如 template C<int i> {}。
- C# 不支持显式定制化;即特定类型模板的自定义实现。
- C# 不支持部分定制化:部分类型参数的自定义实现。
- C# 不允许将类型参数用作泛型类型的基类。
- C# 不允许类型参数具有默认类型。
- 在 C# 中,泛型类型参数本身不能是泛型,但是构造类型可以用作泛型。 C++ 允许使用模板参数。
- C++ 允许在模板中使用可能并非对所有类型参数有效的代码,随后针对用作类型参数的特定类型检查此代码。 C# 要求类中编写的代码可处理满足约束的任何类型。 例如,在 C++ 中可以编写一个函数,此函数对类型参数的对象使用算术运算符 + 和 -,在实例化具有不支持这些运算符的类型的模板时,此函数将产生错误。 C# 不允许此操作;唯一允许的语言构造是可以从约束中推断出来的构造。