泛型
泛型的优点:
- 性能
使用泛型可以避免值类型和引用类型转换时的装箱和拆箱操作 - 类型安全
使用泛型可以定义允许使用的类型,而不是使用object - 少写点代码
泛型类可以定义一次,但使用许多不同的类型实例化
命名约定
- 泛型类型的名称用字母T作为前缀
- 如果没有特殊的要求,且只使用了一个泛型类型,就可以用字符T作为泛型类型的名称
- 如果泛型类型有特定的要求(例如,它必须实现一个接口或派生自基类),或使用了两个或多个泛型类型,就应该给泛型类型使用描述性的名称
public delegate void EventHandler<TEventArgs>(object sender,TEventArgs e);
public delegate TOutput Converter<TInput,TOutput>(TInput from);
1.泛型类
public class MyClass<T>
{
private T _myField;
public T MyField {
get => _myField;
set =>_myField = value;
}
public MyClass(T value)
{
_myField = value;
}
public T GetValue()
{
return _myField;
}
}
1.1 默认值
private T _myField=default;
default在c# 7.1及以上才可以使用
1.2 约束
public class MyClass<T>
where T:class
{
}
上面的约束含义是T必须是引用类型
泛型支持的几种约束类型,如下表所示:
约束 | 说明 |
---|---|
where T:struct | 对于结构约束,类型T必须是值类型 |
where T:class | 类约束指定类型T必须是引用类型 |
where T:IFoo | 指定类型T必须实现接口IFoo |
where T:Foo | 指定类型T必须派生自基类Foo |
where T:new() | 构造函数约束,指定类型T必须有一个默认构造函数 |
where T1:T2 | 指定类型T1派生自泛型类型T2 |
注:可以使用多个约束
1.3 继承
public abstract class Base<T>
{
public abstract T Test(T x, T y);
}
public class Derived<T> : Base<T> //或:Base<int>
{
public override T Test(T x, T y)
{
T a = default;
return a;
}
}
泛型类型可以实现泛型接口,也可以派生自一个类,其要求是必须重复接口的泛型类型,或者必须指定基类的类型。
2.泛型接口
泛型接口:
public interface IComparable<T>
{
int CompareTo(T other);
}
2.1 协变和逆变
官方:https://docs.microsoft.com/zh-cn/dotnet/csharp/programming-guide/concepts/covariance-contravariance/
其他:https://www.sohu.com/a/337520881_120050810
//继承
string str = "test";
object obj = str;
//协变
IEnumerable<string> strings = new List<string>();
IEnumerable<object> objects = strings;
//逆变
Action<object> actObject = SetObject;
Action<string> actString = actObject;
总结:
1.协变和逆变只能在泛型接口和委托中使用,且泛型类型T只能是引用类型,不能是值类型。
//报错:法将类型“System.Collections.Generic.List<int>”隐式转换
//为“System.Collections.Generic.IEnumerable<object>”。存在一个显式转换(是否缺少强制转换?)
IEnumerable<object> objects = new List<int>();
2.协变用out关键字标注泛型类型T,T只能做方法的返回类型;逆变用in关键字标注泛型类型T,T只能做方法的参数。
3.协变允许方法具有的返回类型比接口的泛型类型参数定义的返回类型的派生程度更大,即可以这样IFoo<obejct> b=IFoo<string> a
;
逆变允许方法具有的实参类型比接口的泛型形参定义的类型的派生程度更小,即可以这样IFoo<string> b=IFoo<obejct> a
。具体情况如下:
协变:
public interface IFoo<out T>
{
T Foo();
}
public class ClassA<T> : IFoo<T>
{
private T a;
public ClassA(T b)
{
a = b;
}
public T Foo()
{
return a;
}
}
static void Main(string[] args)
{
IFoo<string> a = new ClassA<string>("x");
IFoo<object> b = a;
//b.Foo实际的方法签名是 string Foo(),b.Foo()返回的是string类型
//这里将string隐式转换为object
object obj = b.Foo();
}
逆变
public interface IFoo<in T>
{
void Foo(T b);
}
public class ClassA<T> : IFoo<T>
{
private T a;
public void Foo(T b)
{
a = b;
}
}
static void Main(string[] args)
{
IFoo<object> a = new ClassA<object>();
IFoo<string> b = a;
//同样是将string转换为object
b.Foo("x");
}
3.泛型结构
泛型结构和泛型类类似,只是没有继承特性。
可空类型Nullable<T>的一个简化版本:
public struct Nullable<T>
where T : struct
{
private T _value;
private bool _hasValue;
public T Value
{
get
{
if (!_hasValue)
{
throw new InvalidOperationException("No Value");
}
return _value;
}
}
public bool HasValue
{
get {
return _hasValue;
}
}
public Nullable(T value)
{
_value = value;
_hasValue = true;
}
public static explicit operator T(Nullable<T> value)=>value.Value;
public static implicit operator Nullable<T>(T value)=>new Nullable<T>(value);
}
static void Main(string[] args)
{
Nullable<int> x;
//无法将 null 转换为“Nullable<int>”,因为后者是不可以为 null 的值类型
x=null;
}
实际上,你如果按照上面自己写了一个Nullable<T>的结构,会发现它是不可以为null的,因为结构是值类型,官方解释看https://docs.microsoft.com/zh-cn/dotnet/api/system.nullable?redirectedfrom=MSDN&view=net-5.0,
在其它地方看到的一个解释:
clr 会在装箱时将一个 HasValue 值为 false 的 Nullable<T> 结构体变成 null 引用。相反地,在将 null 拆箱成 Nullable<T> 时,会转成一个 HasValue 值为 false 的 Nullable<T> 实例。这件事是 clr 开的后门,因此 Nullable<T> 一定是在 mscorlib.dll 中定义的。其他与 null 有关的行为都是编译器干的,与 clr 无关。
//null引用拆箱为NUllable<T>
int? x = null;
if (!x.HasValue)
{
Console.WriteLine("x is null");
}
4.泛型方法
4.1 示例
static T Foo<T>(T x, T y)
where T:IComparable //约束
{
return x.CompareTo(y) > 0 ? x : y;
}
static void Main(string[] args)
{
//Console.WriteLine(Foo<int>(3,2));
//调用时可以不传泛型类型,编译器会根据参数类型推断出泛型类型
Console.WriteLine(Foo(3,2));
}
4.2 带委托的泛型方法
static T1 Accumulate<T1>(IEnumerable<T1> source, Func<T1,T1,T1> action)
{
T1 sum = default(T1);
foreach (T1 item in source)
{
sum = action(item, sum);
}
return sum;
}
static void Main(string[] args)
{
int amount = Accumulate<int>(new int[] { 1, 2, 3, 4, 5 }, (item, sum) => sum += item);
Console.WriteLine(amount);
}
4.3 泛型方法重载及规范
static void Foo<T>(T x)
{
Console.WriteLine("Foo<T>(T x)");
}
static void Foo(int x)
{
Console.WriteLine("Foo(int x)");
}
static void Bar<T>(T x)
{
Foo(x);
}
static void Main(string[] args)
{
Foo(1);
Bar(1);
}
输出
Foo(int x)
Foo<T>(T x)
1.在编译期间,会使用最佳匹配,如果传一个int,就优先选择带int参数的方法,而不是泛型方法。
2.选择调用方法是在编译期间而不是运行期间定义的,上面调用Bar<T>(T x),虽然传入的是int类型,但是调用的却是泛型方法。
5. 泛型委托
泛型委托和泛型方法类似,不过可以使用协变和逆变。
public delegate T DFoo<out T>() where T:IComparable;
常见的泛型委托:
Func
//没有参数,返回TResult类型的值
public delegate TResult Func<out TResult>();
//该方法具有一个参数,且返回由 TResult 参数指定的类型的值。
public delegate TResult Func<in T,out TResult>(T arg);
更多重载方法见:https://docs.microsoft.com/zh-cn/dotnet/api/system.func-1?view=net-5.0
Action
//封装一个方法,该方法不具有参数且不返回值。
public delegate void Action();
//封装一个方法,该方法只有一个参数并且不返回值。
public delegate void Action<in T>(T obj);
更多重载方法见:https://docs.microsoft.com/zh-cn/dotnet/api/system.action?view=net-5.0
Predicate
//表示一种方法,该方法定义一组条件并确定指定对象是否符合这些条件。
public delegate bool Predicate<in T>(T obj);