C#泛型的逆变协变(个人理解)

yizhihongxing

前编

一般来说, 泛型的作用就类似一个占位符, 或者说是一个参数, 可以让我们把类型像参数一样进行传递, 尽可能地复用代码

我有个朋友, 在使用的过程中发现一个问题

IFace<object> item = new Face<string>(); // CS0266

public interface IFace<T>
{
    string Print(T input);
}
public class Face<T> : IFace<T>
{
    public string Print(T input) => input.ToString();
}

Q:   string 明明是 object 的子类, 为啥这样赋值会报错呢???
A:   因为 Face<string> 实现的是 IFace<string>, 而 IFace<string> 并不是 IFace<object> 的子类
Q:   但是 stringobject 的子类啊, IFace<string> 可不就是 IFace<object> 吗?
A:   如果只论接口定义, 看起来确实是这样的, 但是你要看内部实现的方法, IFace<string>Print 方法参数是 string, 但是 IFace<object>Print 参数是 object, 如果上面的赋值可以成立, 就意味着允许 Print(string input) 方法传递任意类型的对象, 这样明显是有问题的
Q:   但是我曾经看到过 IEnumerable<object> list = new List<string>(); 这个为什么就可以
A:   这就要讲到C#泛型里的逆变协变
Q:   细嗦细嗦

逆变协变

C#泛型中的逆变(in)协变(out)对于不常自定义泛型的开发来说(可能)是个很难理解的概念, 简单来说其表现形式如下

逆变(in): I<子类> = I<父类>
协变(out): I<父类> = I<子类>

上面例子中提到的 IEnumerable<object> list = new List<string>(); 体现的是协变, 符合一般直觉, 整体上看起来就像是将子类赋值给基类

转到 IEnumerable<> 的定义, 我们可以看到

public interface IEnumerable<out T> : IEnumerable
{
    new IEnumerator<T> GetEnumerator();
}

泛型 T 之前加了协变的关键词 out, 代表支持协变, 可以进行符合直觉且和谐的转化

前编中提到的代码例子不适用并且也不能改造成协变, 只适合使用逆变

相比于符合直觉且和谐协变, 逆变不符合直觉并且别扭

IFace<string> item = new Face<object>();

public interface IFace<in T>
{
    string Print(T input);
}
public class Face<T> : IFace<T>
{
    public string Print(T input) => input.ToString();
}

这是一个逆变的例子, 与协变相似, 需要在泛型 T 之前加上关键词 in

对比上方的协变, 逆变看起来就像是将基类赋值给子类, 但这其实符合里氏代换的

当我们调用 item.Print 时, 看起来允许传入的参数为 string 类型, 而实际上最终调用的 Face<object>.Print 是支持 object 的, 传入 string 类型的参数没有任何问题

逆变协变的作用

逆变(in)协变(out)的作用就是扩展泛型的用法, 帮助开发者更好地复用代码, 同时通过约束限制可能会出现的破坏类型安全的操作

逆变协变的限制

虽然上面讲了逆变(in)协变(out)看起来是什么样的, 但我的那个朋友还是有些疑问

Q:   那我什么时候可以用逆变, 什么时候可以用协变, 这两个东西用起来有什么限制?
A:   简单来说, 有关泛型输入的用逆变, 关键词是in, 有关泛型输出的用协变, 关键词是out, 如果接口中既有输入又有输出, 就不能用逆变协变
Q:   为什么这两个不能同时存在?
A:   协变的表现形式为将子类赋值给基类, 当进行输出相关操作时, 输出的对象类型为基类, 是将子类转为基类, 你可以说子类是基类;
逆变的表现形式为将基类赋值给子类, 当进行输入相关操作时, 输入的对象为子类, 是将子类转为基类, 这个时候你也可以说子类是基类;
如果同时支持逆变协变, 若先进行子类赋值给基类的操作, 此时输出的是基类, 子类转为基类并不会有什么问题, 但进行输入操作时就是在将基类转为子类, 此时是无法保证类型安全的;
Q:   听不懂, 能不能举个例子给我?
A:   假设 IEnumerable<> 同时支持逆变协变, IEnumerable<object> list = new List<string>();进行赋值后, list中实际保存的类型是string, item.First()输出类型为object, 实际类型是string, 此时说stringobject没有任何问题, 协变可以正常发挥作用;
但是如果支持了逆变, 假设我们进行输入类型的操作, item.Add() 允许的参数类型为 object, 可以是任意类型, 但是实际上支持string类型, 此时的object绝无可能是string
Q:   好像听懂了一点了, 我以后慢慢琢磨吧

两者的限制简单总结就是

输入的用逆变
输出的用协变

原文链接:https://www.cnblogs.com/CollapseNav/p/17285595.html

本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:C#泛型的逆变协变(个人理解) - Python技术站

(0)
上一篇 2023年4月18日
下一篇 2023年4月18日

相关文章

  • C#.NET实现网页自动登录的方法

    下面我会详细讲解如何使用C#.NET实现网页自动登录的方法。 一、需要用到的工具和技术 HttpWebRequest类:C#中提供的Http请求发送类,用于请求Web页面并获取响应数据。 CookieContainer类:C#中提供的Cookie容器,用于管理发送和接收的Cookie信息。 HtmlAgilityPack类库:第三方类库,用于解析HTML文本…

    C# 2023年6月1日
    00
  • C#微信小程序服务端获取用户解密信息实例代码

    下面我将为大家详细讲解“C#微信小程序服务端获取用户解密信息实例代码”的完整攻略。 1.获取微信小程序用户加密数据 在开发微信小程序时,我们需要获取用户的加密数据。这些数据需要在客户端通过 wx.login() 方法获取到 code,然后传到服务端,服务端再根据 code 去微信服务器换取用户的 session_key。而用户数据是加密的,需要使用 sess…

    C# 2023年6月1日
    00
  • Winform使用FTP实现自动更新

    Winform使用FTP实现自动更新可以分为以下几个步骤: 1. 建立FTP连接 使用System.Net.WebRequest类来建立FTP连接。首先,需要先定义一个Uri对象来指定FTP服务器的URL地址,可以使用ftp://开头的URL地址,然后通过WebRequest.Create()方法来创建WebRequest对象,将Uri对象作为参数传入。接着…

    C# 2023年5月31日
    00
  • C#影院售票系统毕业设计(1)

    下面逐步讲解“C#影院售票系统毕业设计(1)”的完整攻略。 1. 确定项目需求 要开发一个影院售票系统,需要满足以下需求: 用户端 用户可以通过图形化操作,实现选座、支付等功能; 可以查看影片排片信息、影院信息、个人信息等; 可以对已完成订单进行评价和投诉; 实现基本的安全性,如密码保护等。 管理员端 管理员可以通过图形化界面添加、修改、删除影片和影院信息;…

    C# 2023年6月1日
    00
  • 深入C# 内存管理以及优化的方法详解

    深入C# 内存管理以及优化的方法详解 在C#中,内存管理是一个非常重要的话题。由于C#运行于托管环境中,所以我们通常不需要手动管理内存。但是,仍然有一些情况需要我们了解和优化内存管理。本文将为你详细探讨C#内存管理和优化的方法,同时会提供一些示例。 内存管理 通常情况下,C#的垃圾回收器(GC)会自动管理内存。GC会自动回收无用的对象,并且为我们管理内存。但…

    C# 2023年6月7日
    00
  • 关于C# 5.0 CallerMemberName CallerFilePath CallerLineNumber 在.NET4中的使用介绍方法

    C# 5.0 CallerMemberName CallerFilePath CallerLineNumber 在.NET4中的使用介绍方法 在C# 5.0及以上版本中,通过使用CallerMemberName、CallerFilePath和CallerLineNumber特性,可以在方法内部获取调用该方法的成员、代码文件名以及代码行号等信息,便于对代码进行…

    C# 2023年6月1日
    00
  • C# CLR学习 C++使用namespace实例详解

    C# CLR学习 C++使用namespace实例详解 简介 在学习C++时,我们经常使用 namespace 来管理我们的代码库。而在使用C++/CLI调用C++代码时,我们需要了解一些关于 namespace 的知识以及如何在C#中使用C++的 namespace。 namespace的作用 namespace 的作用是在C++中帮助我们组织代码,防止命…

    C# 2023年6月7日
    00
  • C# AddRange(ICollection):将 ICollection 中的元素添加到集合中

    C#中的AddRange方法用于将一个集合的元素添加到另一个集合中。其定义如下: public void AddRange(ICollection collection) 可以看到,它接受一个ICollection类型的参数,也就是说可以接收任何实现ICollection接口的类的对象,比如List、HashSet、Queue等等。 具体使用方法如下: Li…

    C# 2023年4月19日
    00
合作推广
合作推广
分享本页
返回顶部