C#.Net筑基-泛型T & 协变逆变
<p><img src="https://img2024.cnblogs.com/blog/151257/202506/151257-20250621220510503-669895124.png" alt="" loading="lazy"></p><h1 id="01什么是泛型">01、什么是泛型?</h1>
<p>泛型(Generics)是C#中的一种强大的强类型扩展机制,在申明时用“占位符”类型参数“T”定义一个“模板类型”,比较类似于C++中的模板。泛型在使用时指定具体的T类型,从而方便的封装、复用代码,提高类型的安全性,减少类型转换和装箱。</p>
<p><img src="https://img2024.cnblogs.com/blog/151257/202506/151257-20250621220510509-39414514.png" alt="" loading="lazy"></p>
<ul>
<li>泛型就是为代码能 <font style="color: rgba(223, 42, 63, 1)">跨类型复用</font> 而设计的,轻松复用代码逻辑,如<code>List<T></code>、<code>Queue<T></code>。</li>
<li>用泛型参数来代替object,可以减少大量装箱、拆箱,显著提高代码性能,及代码安全性。比如C#中的<code>List<T></code>就是泛型版的<code>ArrayList</code>,<code>Dictionary<TKey, TValue></code>就是泛型版的<code>Hashtable</code>,非泛型版本就不建议使用了。</li>
</ul>
<h2 id="11泛型知识点集合">1.1、泛型知识点集合</h2>
<table>
<thead>
<tr>
<th><strong>知识点</strong></th>
<th><strong>说明</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>泛型类型</td>
<td>类、结构体、接口、委托:申明时类型名后指定一个或多个泛型参数,<code>class User<T,TV,TP>{}</code>。</td>
</tr>
<tr>
<td>泛型方法</td>
<td>在方法上指定泛型参数,<code>public T Add<T>(T x,T y){}</code>。</td>
</tr>
<tr>
<td>泛型参数“T”</td>
<td>用尖括号<code><T></code>的语法引入泛型参数“T",表示这是一个泛型类、或泛型方法,支持一个或多个泛型参数。</td>
</tr>
<tr>
<td>泛型参数“T”命名</td>
<td>一般用“T”或T开头来占位,表示一个模板类型,名称可自定义</td>
</tr>
<tr>
<td>泛型约束where</td>
<td>对泛型参数T的条件约束,限定T的类型、范围,更方便的封装代码 <code>class User<T> where T : struct</code></td>
</tr>
<tr>
<td>开放类型 <code>List<T></code></td>
<td>未指定泛型参数的类型叫“开放类型”,不能直接使用。<font style="color: rgba(223, 42, 63, 1)">只有身体,没有灵魂,并不完整</font>。</td>
</tr>
<tr>
<td>封闭类型 <code>List<int></code></td>
<td>指定了泛型参数后的泛型为“封闭类型”,才是完整的类型,才可以实例化,这里的泛型参数为<code>int</code>。</td>
</tr>
<tr>
<td>静态成员共享</td>
<td>泛型类型中的静态成员,所有封闭类型是共享的。</td>
</tr>
</tbody>
</table>
<ul>
<li>构造函数不可引入泛型参数。</li>
<li>不同数量的泛型参数可以“重载”,<code>interface IUser<T></code>,<code>interface IUser<T1,T2></code>是不同的两个泛型类型。</li>
</ul>
<p><img src="https://img2024.cnblogs.com/blog/151257/202506/151257-20250621220510530-566187354.jpg" alt="画板" loading="lazy"></p>
<hr>
<h1 id="02泛型约束where">02、泛型约束where⭐</h1>
<p>如果没有约束,泛型参数“T”可以用任何类型来替代。泛型约束可以约束泛型参数“T”的范围,然后可以利用约束类型的一些能力,这也是泛型比较强大的地方之一。</p>
<ul>
<li>约束条件用<code>where:[约束1],[约束2]</code>语法申明,跟在泛型申明后面,可以跟多个约束条件,逗号隔开。</li>
<li>多个泛型参数,可用多个<code>where</code>分别约束。</li>
</ul>
<table>
<thead>
<tr>
<th><strong>泛型约束条件</strong></th>
<th><strong>约束说明</strong></th>
</tr>
</thead>
<tbody>
<tr>
<td>class</td>
<td>必须为引用类型,可以是任何类、接口、委托、数组。</td>
</tr>
<tr>
<td>struct</td>
<td>必须为非null值类型。</td>
</tr>
<tr>
<td>notnull</td>
<td>不为 null 的类型。</td>
</tr>
<tr>
<td>unmanaged</td>
<td>“非托管类型”<font style="color: rgba(22, 22, 22, 1)">类型</font>,内置的基础值类型如byte、int、char、float、double、bool、枚举、指针等。</td>
</tr>
<tr>
<td>new()</td>
<td>类型必须有无参构造函数,当与其他约束一起使用时,new() 约束必须在最后。</td>
</tr>
<tr>
<td>Delegate</td>
<td>类型必须为委托</td>
</tr>
<tr>
<td><font style="color: rgba(0, 0, 0, 1)">Enum</font></td>
<td>枚举类型</td>
</tr>
<tr>
<td><code>INumber<T></code></td>
<td>类型为内置数值类型,如<code>int</code>、<code>double</code>,<font style="color: rgba(210, 45, 141, 1)">这个不错</font>,非常便于封装数值相关的代码!如数学运算。</td>
</tr>
<tr>
<td>具体接口、类型</td>
<td>任意明确的类型、接口作为约束条件,不能是<font style="color: rgba(223, 42, 63, 1)">封闭类型</font>(封闭类型用泛型就没有意义了)</td>
</tr>
<tr>
<td>其他泛型类型</td>
<td>约束类型可以继续用其他泛型类型,<code>where T2 : IUser<T2></code></td>
</tr>
<tr>
<td>相互约束</td>
<td>泛型参数之间相互约束,<code>class MyClass<T, U> where T : U</code> 约束类型<code>T</code> 和<code>U</code>兼容</td>
</tr>
<tr>
<td>自引用约束</td>
<td>用自身类型作为约束,<code>interface IUser<T> where T : IUser<T></code></td>
</tr>
</tbody>
</table>
<blockquote>
<p>📢 并不是所有类型都可以用于约束,<strong><font style="color: rgba(210, 45, 141, 1)">严格来说只有接口、未封闭的类才能用与类型约束</font></strong>,不支持的有<code>int</code>、<code>float</code>、<code>double</code>、<code>Array</code>、数组、<code>ValueType</code>等,Object是万物基类,也不能作为约束,这个约束等于没有约束。</p>
</blockquote>
<ul>
<li>泛型约束最大的好处就是可以利用约束的能力,实现更方便的封装。</li>
</ul>
<pre><code class="language-csharp">public T Create<T>() where T : new()//约束了T可以new,就是具备无参构造函数
{
return new T();//这里就可用这个约束能力了
}
public T Max<T>(T a, T b) where T : IComparable<T> //约束实现了比较接口
{
return a.CompareTo(b) > 0 ? a : b;
}
// Max(1,2); //2
public class GClass<T, TV>
where T : IComparable<T>, new()
where TV : struct, INumber<TV> //数值类型,可以用数学运算了
{
public TV Value { get; set; }
public void Add(TV value)
{
this.Value += value;
}
}
</code></pre>
<blockquote>
<p>📢 <font style="color: rgba(22, 22, 22, 1)">.NET 7 中实现的 </font>INumber-TSelf 添加了大量数学运算接口,C#内置的数值类型(int、float、double等)都实现了该接口。官方文档《泛型数学》还有更多更细的数学运算接口。</p>
</blockquote>
<hr>
<h1 id="03协变与逆变">03、协变与逆变</h1>
<p>C#中的协变与逆变,本质就是灵活控制类型的向上(父类)转换,即保障类型安全,又兼顾灵活性和代码的复用性。这里就不得不回顾下向对象的基本原则之一——里氏替换原则。</p>
<h2 id="31里氏替换原则">3.1、里氏替换原则</h2>
<p><strong>里氏替换原则</strong> (Liskov Substitution Principle,LSP)是面向对象编程中的基本原则之一,在各种编程语言中使用广泛。其定义为:<strong><u><font style="color: rgba(210, 45, 141, 1)">派生类(子类)对象可以代替其基类(超类)对象</font></u></strong>,这也是面向对象多态的体现。</p>
<p><img src="https://img2024.cnblogs.com/blog/151257/202506/151257-20250621220510515-1584264908.jpg" alt="画板" loading="lazy"></p>
<p>就是说任何子类都可以替代其父类,或者说子类可以安全的转换为父类。在接口或类的继承中,向上转换是安全的,这也是继承的基本特点。如下面的方法<code>Foo(object value)</code>,可以传入任意<code>object</code>的子类,因为所有类型都继承自<code>object</code>。</p>
<pre><code class="language-csharp">void Main()
{
string s = null;
object o = s; //父类兼容子类,string 隐式转换为 object
Foo("sam"); //类型匹配 string隐式转换为object
Foo(new User()); //类型匹配 User隐式转换为object
Foo<string>("sam");//类型一致
Foo<object>("sam");//类型匹配 string隐式转换为object
}
public object Foo(object obj)
{
return "sam";//string隐式转换为object
}
public void Foo<T>(T obj) { }
</code></pre>
<p>日常编程中也常常用到里氏替换原则,用子类代替父类使用。</p>
<pre><code class="language-csharp">void Main()
{
SetUser(new User());
SetUser(new Teacher()); //输入参数用子类代替
}
public class User { }
public class Teacher : User { }
public User FindUser(){
return new Teacher(); //返回值用子类代替
}
public void SetUser(User user){}
</code></pre>
<p>在C#中,<strong>里氏替换原则</strong>的表现就是子类可以隐式转换为父类,如上面的示例,在方法调用、返回值、赋值时都支持向上的隐式转换。但是在泛型中,这却行不通,如下示例代码,这就不符合上面的里氏替换原则了,影响了编程的灵活性。</p>
<pre><code class="language-csharp">interface IFoo<T>{}
IFoo<string> s2 = default;
IFoo<object> o2 = s2; //不可隐式转换,报错
//添加out参数后,可隐式转换
interface IFoo<out T>{}
</code></pre>
<blockquote>
<p>📢 在泛型中,是需要严格类型匹配的,才能保障类型的安全。在某些场景但为了兼顾灵活性、复用性,便有了协变、逆变。</p>
</blockquote>
<h2 id="32协变covarianceout逆变contravariancein">3.2、协变(Covariance/out)、逆变(Contravariance/in)</h2>
<p>为了在泛型中支持上述隐式转换,就有协变、逆变。当然这不仅仅用于泛型,委托中的协变、逆变和泛型是一样的,还有C#中的数组是支持协变的。</p>
<ul>
<li><strong>协变(Covariance)</strong>:用<code><font style="color:#D22D8D;">out</font></code> 关键字指定类型参数是协变的,用于输出参数,如方法的返回值类型。表现为子类隐式转换为父类,就是标准的里式替换原则。</li>
<li><strong>逆变(Contravariance)</strong>:用<code><font style="color:#D22D8D;">in</font></code>关键字指定类型参数是逆变的,一般用于输入,如方法的参数。表现为协变相反的转换过程,但其实本质上(在方法参数上)还是里式替换原则。</li>
</ul>
<blockquote>
<p>📢<code>out</code>、<code>in</code>关键字只能用在泛型接口、泛型委托上。</p>
</blockquote>
<p>下面是C#中内置的协变、逆变使用场景。</p>
<pre><code class="language-csharp">//数组是内置支持协变的,只支持引用类型,不支持值类型
object[] arr = new string;
object[] us = new User;
//C#中的IEnumerator<T>源码
public interface IEnumerator<out T> : IDisposable, IEnumerator
{
new T Current { get; }
}
//C#中的Func<T1,T2,TResult>源码
public delegate TResult Func<in T1, in T2, out TResult>(T1 arg1, T2 arg2);
</code></pre>
<p><strong>🌰协变<code>out</code>的示例代码</strong>:</p>
<ul>
<li><code>IFoo<string></code> 隐式转换为<code>IFoo<object></code>,子类(泛型)转父类(泛型),协变。</li>
<li>在方法返回值上,<code>IFoo<object>.Func()</code> 返回<code>object</code>,支持返回<code>string</code>(<code>IFoo<object></code>),<code>string</code>转<code>object</code>,是符合<strong>里氏替换原则</strong>的。</li>
</ul>
<pre><code class="language-csharp">void Main()
{
IFoo<string> f1 = new Foo();
IFoo<object> f2 = f1;//协变,IFoo<string> 隐式转换为IFoo<object>
}
interface IFoo<out T>//如果没有out,则上面的转换抛出异常
{
public T Func();
}
class Foo : IFoo<string>
{
public string Func()
{
return "sam";
}
}
</code></pre>
<p><img src="https://img2024.cnblogs.com/blog/151257/202506/151257-20250621220510509-2066204256.png" alt="" loading="lazy"></p>
<p><img src="https://img2024.cnblogs.com/blog/151257/202506/151257-20250621220459874-765896259.png" alt="image" loading="lazy"></p>
<p><strong>🌰逆变<code>in</code>的示例代码</strong>:</p>
<ul>
<li><code>IFoo<object></code> 隐式转换为<code>IFoo<string></code>,父类(泛型)转子类(泛型),相反的方向,逆变。</li>
<li>在方法参数上,<code>IFoo<object></code> 参数为<code>object</code>,支持用<code>string</code>(<code>IFoo<string></code>),<code>string</code>转<code>object</code>,是符合<strong>里氏替换原则</strong>的。</li>
</ul>
<pre><code class="language-csharp">void Main()
{
IFoo<object> f1 = new Foo();
IFoo<string> f2 = f1;//逆变,IFoo<object> 隐式转换为IFoo<string>
}
interface IFoo<in T>//如果没有in,则上面的转换抛出异常
{
public void Func(T value);
}
class Foo : IFoo<object>
{
public void Func(object value) { }
}
</code></pre>
<blockquote>
<p>warning<br>
📢 协变、逆变只是其表象,其本质是一样的,就是<strong>里氏替换原则!</strong>(可用子类替换为父类)</p>
</blockquote>
<hr>
<h1 id="参考资料">参考资料</h1>
<ul>
<li>.NET 中的泛型</li>
<li>类型参数的约束(C# 编程指南)</li>
<li>深入理解C#的协变和逆变及其限制原因</li>
<li>《C#8.0 In a Nutshell》</li>
</ul>
<hr>
<blockquote>
<p><strong>©️版权申明</strong>:版权所有@安木夕,本文内容仅供学习,欢迎指正、交流,转载请注明出处!<em>原文编辑地址-语雀</em>__</p>
</blockquote><br><br>
来源:https://www.cnblogs.com/anding/p/18940828
頁:
[1]