温故而知新-C#泛型

泛型类

除了有泛型方法,还有泛型类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class DocumentManager<T>
{
private readonly Queue<T> _documentQueue= new Queue<T>();
private readonly object _lock = new object();

public bool IsAvailable =>_documentQueue.Count > 0;

public void AddDocument(T doc)
{
lock (_lock)
{
_documentQueue.Enqueue(doc);
}
}

public T GetDocument()
{
T doc = default;
lock (_lock)
{
doc=_documentQueue.Dequeue();
}

return doc;
}
}

默认值

可以看到在GetDocument这个方法中初始化doc变量时,我们使用了default关键字

default关键字根据上下文可以有多种含义,switch语句中使用default定义默认情况。在泛型中取决于泛型类型是值类型还是引用类型,泛型default关键字将泛型类型初始化为null或0

泛型接口

泛型类都有了,泛型接口也可以有

1
2
3
4
5
6
public interface IDocumentManager<T>
{
void AddDocument(T doc);

T GetDocument();
}

还有泛型委托

1
public delegate void Hello<T>(T t);

注意

1、泛型在声明的时候可以不指定具体的类型,在使用时需要指定具体类型

1
public class AClass:BClass<int>{}

2、如果子类也是泛型的,那么继承的时候可以不指定具体类型

1
public class AClass<T>:BClass<T>{}

逆变和协变

.NET4.0之前,泛型接口是不变的。

协变逆变指对参数和返回值的类型进行转换。只能放在接口或委托的泛型参数前面

out协变 covariant 用来修饰返回值;

in逆变 contravariant 用来修饰传入参数;

示例:

定义一个Animal类,再定义一个Cat类继承Animal

1
2
3
4
5
6
7
8
9
public class Animal
{
public int Id { get; set; }
}

public class Cat : Animal
{
public string Name { get; set; }
}

调用:

1
2
3
4
5
6
7
8
9
10
Animal animal = new Animal();
Cat cat = new Cat();

Animal animal1 = new Cat();

List<Animal> animals = new List<Animal>();

List<Cat>cats= new List<Cat>();

List<Animal> list = new List<Cat>(); // 报错 没有父子级关系

这个时候可以使用协变

1
IEnumerable list = new List<Cat>();

image-20211008001058082

可以看到,泛型接口使用了out参数修饰,T只能是返回值类型,不能作为参数类型。使用协变后,左边声明的是基类,右边可以声明子类或基类。

协变也可用于委托:

1
Func<Animal>func = new Func<Cat>(()=>null);

自定义协变

out协变,只能是返回结果

1
2
3
4
5
6
7
8
9
10
11
12
public interface ICustomerListOut<out T>
{
T Get();
}

public class CustomerListOut<T> : ICustomerListOut<T>
{
public T Get()
{
return default(T);
}
}
1
ICustomerListOut<Animal> list1 = new CustomerListOut<Cat>();

自定义逆变

in逆变,只能作为方法参数,不能作为返回值。

1
2
3
4
5
6
7
8
9
10
11
public interface ICustomerListIn<in T>
{
void Show(T t);
}
public class CustomerListIn<T> : ICustomerListIn<T>
{
public void Show(T t)
{

}
}
1
ICustomerListIn<Cat> list2 = new CustomerListIn<Animal>();

泛型约束

如果泛型类需要调用泛型类型中的方法,则需要添加约束。

定义一个IDocument接口,定义两个只读属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public interface IDocument
{
string Title { get; }
string Content { get; }
}
public class Document:IDocument
{
public Document(string title,string content)
{
this.Title = title;
this.Content = content;
}
public string Title { get; }
public string Content { get; }
}

如果要显示Document的标题,我们可以这样写:

T强制转换为IDocument接口

1
2
3
4
5
6
7
public void DisplayAllDocuments()
{
foreach (var doc in _documentQueue)
{
Console.WriteLine(((IDocument)doc).Title);
}
}

但是如果我们实现的不是IDocument呢?而是其他的,并没有Title,这个时候就会报错。

这个时候我们可以给DocumentManager一个约束:T类型必须实现IDocument接口,使用Where关键字

1
2
3
4
5
6
7
8
9
DocumentManager<T>:IDocumentManager<T> where T : IDocument

public void DisplayAllDocuments()
{
foreach (var doc in _documentQueue)
{
Console.WriteLine(doc.Title);
}
}

结果:

image-20211007223036959

类型

约束 说明
where T:struct T类型必须是值类型
where T:class T类型必须是引用类型(类、接口、委托、数组等)
where T:IFoo 指定类型必须实现接口IFoo
where T:Foo 指定类型T必须派生自基类Foo
where T:new() 构造函数约束,指定类型T必须有一个默认构造函数,需最后指定
where T1:T2 T1派生自泛型类型T2

new()只能为默认构造函数定义构造函数约束,不能为其他构造函数定义构造函数约束

基类约束时,基类不能是密封类(即sealed类),sealed类不能被继承,则此约束无意义。

泛型约束可以多个约束:

1
public void Show<T>(T t) where T:Document,new(){}

泛型继承

1
public class DocumentManager<T> : IDocumentManager<T> where T : IDocument

泛型类型可以实现泛型接口,也可以派生自一个类。泛型类可以派生自泛型基类:

1
2
3
public class Base<T>{}

public class Derived<T>:Base<T>{}

必须重复实现接口的泛型类型,或者必须执行基类的类型

1
2
3
public class Base<T>{}

public class Derived<T>:Base<string>{}

派生类可以是泛型类非泛型类

例子:定义一个抽象的泛型类,在派生类中用另一种实现

1
2
3
4
5
6
7
8
9
10
11
12
public abstract class Calc<T>
{
public abstract T Add(T x, T y);
}

public class IntCalc : Calc<int>
{
public override int Add(int x, int y)
{
return x + y;
}
}

静态成员

泛型类的静态成员只能在类的一个实例中共享

1
2
3
4
5
6
7
8
public class StaticDemo<T>
{
public static int x;
}

StaticDemo<string>.x = 4;
StaticDemo<int>.x = 5;
Console.WriteLine(StaticDemo<string>.x);

结果:

image-20211007233059047

泛型缓存

我们知道,类的静态构造函数只会执行一次,所以不管无论实例化多少次,在内存中只会有一个。

在泛型中,T类型不同,每个不同的T类型都会生成一个不同的副本,会产生不同的静态属性、静态构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
for (var i = 0; i < 5; i++)
{
Console.WriteLine(GenericCache<int>.GetCache());
Thread.Sleep(10);

Console.WriteLine(GenericCache<long>.GetCache());
Thread.Sleep(10);

Console.WriteLine(GenericCache<string>.GetCache());
Thread.Sleep(10);

Console.WriteLine(GenericCache<DateTime>.GetCache());
Thread.Sleep(10);
}

public class GenericCache<T>
{
private static readonly string _typeTime;
static GenericCache()
{
Console.WriteLine("static");

_typeTime = $"{typeof(T).FullName}_{DateTime.Now:yyyyMMddHHmmss.fff}";
}

public static string GetCache()
{
return _typeTime;
}
}

结果:

image-20211008003511609

泛型会为不同类型都创建一个副本,构造函数执行5次,后面获取的缓存都是相同的。

注意:只能为不同的类型缓存一次。泛型缓存比字典缓存效率高,但是不能主动释放。

泛型结构

与类一样,结构也可以是泛型的。但是不同于类,不能继承。

Nullable<T>为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public struct Nullable<T> where T: struct
{
private bool _hasValue;
public bool HasValue => _hasValue;
public Nullable(T value)
{
_hasValue = true;
_value = value;
}

private T _value;

public T Value
{
get
{
if (!_hasValue)
{
throw new InvalidOperationException("no value");
}
return _value;
}
}

public static explicit operator T(Nullable<T> value) => value.Value;

public static implicit operator Nullable<T>(T value) => new Nullable<T>(value);

public override string? ToString()
{
return !HasValue ? string.Empty : _value.ToString();
}
}

使用:

1
2
3
4
5
6
Nullable<int> x;
x = 4;
if (x.HasValue)
{
Console.WriteLine(x.Value);
}

可空类型

c#中,使用?定义可空类型

1
int? i = 0;
  • 可空类型可以与null或数字比较
  • 可空类型可以与算数运算符使用

非可空类型可以转换成可空类型,隐式转换

1
2
int x = 4;
int? y = x;

可空类型转为非可空类型可能会失败,如果可空类型null赋值给非可空类型则会抛出InvalidOperationException异常,需要强制类型转换

1
2
int? x = GetNullableType(); // 可能返回null
int y = (int)x;

如果不进行显示转换,则可以使用合并运算符从可空类型转换成非可空类型,关键词??,为转换定义一个默认值,以防可控类型的值为null

1
2
int? x = GetNullableType();
int y = x ?? 0;