C#泛型(详解)

前言 

介绍C# 入门经典第8版书中的第12章《泛型》   

一、泛型的含义

为引出泛型的概念,我们先来看看我们前面提到的 集合类icon-default.png?t=N7T8https://blog.csdn.net/qq_71897293/article/details/134684612?spm=1001.2014.3001.5501

这些集合是没有具体类型化的,所以我通常都是把object项转换为集合中实际存储的对象类型。object类型代表所有数据类型都可以储存,但有些时候存储的一些数据类型可能会导致我们程序抛出异常,但我们前面在集合类中都提到了,解决方法。但更好的解决方法,就是创建强类型化的集合类。这种类型的集合派生于CollectionBase。但会有几个问题:

  • 每次创建时需要定义集合类包含的新类型的项
  • 创建后还需要为其提供需要实现的方法

一方面新的类型需要额外的功能,所有每次我们创建集合类时,都会花费大量的时间。另一方面,泛型类大大简化了这个问题,可以快速创建强类型集合,创建“T”类型对象集合,只需要一行代码。

        Collection<Program> collection = new Collection<Program>();

二、使用泛型

1、可空类型

        我们在前面学习过程中,也了解到了很多基本类型,如int、String 其中值类型是不能为null,int就是值类型,所以他的默认值就是0,但我们有时在某些应用场景中需要将int 变为可空类型。可以使用如下代码创建

 System.Nullable<int> @int=null;

注意:创建类型变量时,无论是初始化为null还是通过赋值来初始化,都必须在使用前初始化。

上面的创建代码等同于

        System.Nullable<int> @int =new System.Nullable<int>();

        我们将 @int 变量设置为可空类型,那么它与原来的 int 类型不同的是,它可以为null其他的功能和int 类型一致 。既然可以为null,那我们可以对 @int 进行可空类型判断

class Program
{static void Main(string[] args){System.Nullable<int> @int = new System.Nullable<int>();if (@int != null){}if (@int.HasValue){}//两种方法一致效果}}

注意:

HasValue属性,在我们上面这种判断方式是不可行的。因为引用类型没有初始化,值本身就是null,引用类型的null表示对象不存在,不存在我们调用属性就自然会报错。


属性 Value 如果 HasValue 为true 则代表 Value 有值,反则 HasValue 为false则说明变量被赋值了null,访问value属性就会抛出异常

我们经常在代码中定义可空类型,以至于C#帮我们简化了一种写法 表示相同意思

int? @int;

1.1 运算符和可空类型 

        在简单的类型中,如 int,创建一个int类型的可空类型,这个可空类型和它原来的数据类型没有太大的区别如。+\-\*\/ 使用运算符来计算。

int? @int = 5; int? @int2 = 5;
int? @int3 = @int + int2;

但如果使用以下代码就会无法通过编译。

int @int4 = @int + int3;

只能通过显示转换,或者通过Value属性进行运算。如下示例:

 //无法编译 如果想通过编译 那么只能显示转换int @int4 = (int)(@int + int3);//写法2 通过前面用到的 Value属性 在Nullable<int> 结构当中提供  其中 int?和Nullable<int>效果是一致的 其中int?只不过是他的简写罢了。int @int5 = @int.Value + @int3.Value;//注意这里的Value 有值的前提我们也提到了 是在.HasValue属性为true时才有值,否则为false 

介绍完上面的代码我们在来看看下面这段代码:大家觉得这段运行结果是什么?

int? inta = 5;
int? intb = null;
int? intc = inta * intb;

         对于除bool?外的所有简单可空类型,在值为null的时候,我们统称为不可计算那么问题来了 为什么不能除bool外呢?  因为bool类型的比较通常 就是 && || & | 这四种比较方式,在其中比较的一边为null时 另一边就是整个表达式结果的值。
简要就是 不需要其中一个操作符的值即可计算出结果,则操作数是否为null就不重要。

1.2 ??运算符

        当前运算符的效果,可以进一步缩小我们处理可空类型所需的代码量(引用类型也是可空类型)?? 运算符为空接合运算符  是一个二元运算符 允许给可能等于null的表达式提供另一个值。使用方式:

string Studentzhang = null;
string Studentlong = "龙";
string str = Studentzhang ?? Studentlong;
string str2 = Studentzhang == null ? Studentlong : Studentzhang;

        如果第一个的操作符不是null 那么当前运算符的结果就是第一个操作数,否则,就是第二个操作数。上面的两个表达式的作用是相同的。

        上面的Studentzhang为null时 则str的值就是Studentlong,和下面的三元运算符效果一致。

1.3 ?.运算符

        这个操作符通常被称为Elvis运算符或空条件运算符。实际效果,可以在结果是null时返回一个默认值。如下代码:

 if (Student.student.CollectionBase != null){int count = Student.student.CollectionBase.Count;}

类代码:

 class Student{public static Student student;static Student() { student = student ?? new Student(); }public ArrayList CollectionBase { get; set; }}

上述的代码,对属性进行了判断是否null,如果不为null时,则将数组的数量赋值给变量。但如果您不对属性进行判断,则会抛出异常。下面代码无法执行

 int count1 = Student.student.CollectionBase.Count;

这时我们可以使用空条件运算符。

int? count2=Student.student.CollectionBase?.Count;

        我们在看一段代码,下述代码结合了我们上面讨论到的空合并运算符和现在讲到的空条件运算符以及一个int类型的可空类型。

 int? counts = Student.student.CollectionBase?.Count ?? 0;

 2、System.Collections.Geneir名称空间

在当前名称空间下,我们要介绍其中的两个类型。

类型说明
List<T>                                          T类型对象的集合
Dictionary<K,V>                            与K类型的键值相关的V类型的项的集合

拿其中的List<T>泛型集合来说,我们创建它的实例,就可以使用类。也可以使用我们之前创建集合中,所实现的方法如Add Remove 等,可以大幅度的缩少代码,而且创建十分简单。

System.Collections.Generic.List<T> s = new System.Collections.Generic.List<T>();

注意:这里的T是需要您指定具体类型。

2.1 List<T>使用

下面我将对List集合的使用,提供一个示例:

using System.Collections.Generic;
using static System.Console;namespace Ch12Ex02
{class Program{static void Main(string[] args){List<Animal> animalCollection = new List<Animal>{new Dog("张得帅"),new Chicken("张得酷")};foreach (Animal myAnimal in animalCollection){myAnimal.Feed();}ReadKey();}}public abstract class Animal {protected string name;public string Name{get { return name; }set { name = value; }}public Animal(){name = "没有名字";}public Animal(string newName){name = newName;}public void Feed() => WriteLine($"{name} 在吃饭");}public class Chicken : Animal{public void LayEgg() => WriteLine($"{name} 下了一个蛋");public Chicken(string newName) : base(newName) { }}public class Dog : Animal{public void Crap() => WriteLine($"{name} 拉了一坨屎");public Dog(string newName) : base(newName) { }}
}

        其中特别的是  List<Animal> animalCollection = new List<Animal> 我们只用一行就能创建一个集合,把当前集合指定为Animal的类型集合。获取相同效果,我们可以改一下Animal的定义,将它继承List<Animal>也就是说Animal类本身可以被视为一个Animal对象的列表。 可以更容易看懂,还可以在适当的时候给Animal添加成员。如下所示:

   public abstract class Animal : List<Animal>{protected string name;public string Name{get { return name; }set { name = value; }}public Animal(){name = "没有名字";}public Animal(string newName){name = newName;}public void Feed() => WriteLine($"{name} 在吃饭");}

2.2 对泛型列表进行排序和搜索

这一段书中是详细介绍一段示例代码,如下代码是书中的示例代码。

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using static System.Console;
using static System.Math;
namespace Ch12Ex03
{class Program{static void Main(string[] args){Vectors route = new Vectors{new Vector(2.0, 90.0),new Vector(1.0, 180.0),new Vector(0.5, 45.0),new Vector(2.5, 315.0)};WriteLine(route.Sum());route.Sort(VectorDelegates.Compare);WriteLine(route.Sum());Predicate<Vector> searcher = new Predicate<Vector>(VectorDelegates.TopRightQuadrant);Vectors topRightQuadrantRoute = new Vectors(route.FindAll(searcher));WriteLine(topRightQuadrantRoute.Sum());ReadKey();}}public class Vectors : List<Vector>{public Vectors(){}public Vectors(IEnumerable<Vector> initialItems){foreach (Vector vector in initialItems){Add(vector);}}public string Sum(){StringBuilder sb = new StringBuilder();Vector currentPoint = new Vector(0.0, 0.0);sb.Append("origin");foreach (Vector vector in this){sb.AppendFormat($" + {vector}");currentPoint += vector;}sb.AppendFormat($" = {currentPoint}");return sb.ToString();}}public static class VectorDelegates{public static int Compare(Vector x, Vector y){if (x.R > y.R){return 1;}else if (x.R < y.R){return -1;}return 0;}public static bool TopRightQuadrant(Vector target){if (target.Theta >= 0.0 && target.Theta <= 90.0){return true;}else{return false;}}}public class Vector{public double? R = null;public double? Theta = null;public double? ThetaRadians{// Convert degrees to radians.get { return (Theta * Math.PI / 180.0); }}public Vector(double? r, double? theta){// Normalize.if (r < 0){r = -r;theta += 180;}theta = theta % 360;// Assign fields.R = r;Theta = theta;}public static Vector operator +(Vector op1, Vector op2){try{// Get (x, y) coordinates for new vector.double newX = op1.R.Value * Sin(op1.ThetaRadians.Value)+ op2.R.Value * Sin(op2.ThetaRadians.Value);double newY = op1.R.Value * Cos(op1.ThetaRadians.Value)+ op2.R.Value * Cos(op2.ThetaRadians.Value);// Convert to (r, theta).double newR = Sqrt(newX * newX + newY * newY);double newTheta = Atan2(newX, newY) * 180.0 / PI;// Return result.return new Vector(newR, newTheta);}catch{// Return "null" vector.return new Vector(null, null);}}public static Vector operator -(Vector op1) => new Vector(-op1.R, op1.Theta);public static Vector operator -(Vector op1, Vector op2) => op1 + (-op2);public override string ToString(){// Get string representation of coordinates.string rString = R.HasValue ? R.ToString() : "null";string thetaString = Theta.HasValue ? Theta.ToString() : "null";// Return (r, theta) string.return string.Format($"({rString}, {thetaString})");}}
}

为了便于理解这是我理解后写的代码:

using System;
using System.Collections;
using System.Collections.Generic;
using System.Windows.Documents;
using static System.Console;
class Program
{static void Main(string[] args){Student student = new Student(){new StudentAge (10),new StudentAge (18),new StudentAge (10),new StudentAge (58),new StudentAge (15)};student.StudentWrile();//排序student.Sort(PRP.MYCompare);student.StudentWrile();Student studentA = new Student(student.FindAll(PRP.Tiaojianpanduan));studentA.StudentWrile();ReadKey();}
}
class Student : List<StudentAge>
{object obj = new object();public void StudentWrile(){foreach (var item in this){WriteLine(item.age);}Console.WriteLine(DateTime.Now.ToString("T"));}public Student(){}public Student(IEnumerable<StudentAge> objects){AddRange(objects);}public void Chaxun(){}}
class StudentAge
{public int age { get; set; }public StudentAge(int age){this.age = age;}public static StudentAge GetStudentAge(object a){if (a is StudentAge){return a as StudentAge;}else{throw new Exception("");}}
}
static class PRP
{public static int MYCompare(object x, object y) => ((StudentAge)x).age - ((StudentAge)y).age;public static bool Tiaojianpanduan(StudentAge studentAge){if (studentAge?.age > 10){return true;}return false;}
}

示例解释:

第一点:上述代码将Student 类继承自 List<StudentAge>,这样可以直接使用列表类提供的方法,并且可以在其基础上扩展自己的功能。提高了代码可读性。这对我们日后的开发可以提高我们的代码阅读性,还可以扩展。

第二点:还用到了FillAll方法 ,方法参数 委托参数。需要是具有int类型的返回值 和两个泛型参数的函数。

第三点:集合中的Sort方法这是我们第三种用法了,前面文章已经结束了两种,一种是无参,一种是传输实现了IComparer接口的实例。

2.3 Dictionary<K,V>

        当前集合的K,与Y为两个类型。其中我将介绍泛型的字典集合怎么使用。即Dictionary集合。实现和上一次我们自定义字典集合的功能。

创建方式(三种)

  //创建方式Dictionary<string, int> studentDictionary = new Dictionary<string, int>();//添加方式studentDictionary.Add("张一", 20);studentDictionary.Add("张二", 50);studentDictionary.Add("张三", 80);//第二种添加方式Dictionary<string, int> keyValuePairs = new Dictionary<string, int>(){//添加方式{ "张一", 20 },{ "张二", 50 },{ "张三", 80 }};//第三种 通过使用索引初始化器 它允许在对象初始化器内部初始化索引Dictionary<string, int> keyValuePairs3 = new Dictionary<string, int>(){//添加方式["张一"] = 20,["张二"] = 50,["张三"] = 80};

示例解释:

当前添加数据,指定了键为string类型,值为int类型 。

第一种的方式:为我们常用的使用Add函数来添加。

第二种方式:不使用Add来添加,这里也没有太多介绍,懂得使用就可。

第三种方式:通过使用索引初始化器 它允许在对象初始化器内部初始化索引。

我们可以使代码更加简洁可以:

 //创建一个集合 并初始化var keyValuePairs3 = new Dictionary<string, int>(){//指定什么键 值为什么["张一"] = 20,["张二"] = 50,["张三"] = 80};//这里使用方法来创建var keyValuePairs4 = GetDictionary();
 static Dictionary<string, int> GetDictionary() => new Dictionary<string, int>() { ["张一"] = 20, ["张二"] = 50, ["张三"] = 80 };

迭代方式:

   //迭代集合中的建集合 foreach (var item in keyValuePairs3.Keys){WriteLine("集合中的键:" + item);}WriteLine("------------------");//迭代集合中的值集合foreach (var item in keyValuePairs3.Values){WriteLine("集合中的值:" + item);}WriteLine("------------------");//迭代集合本身foreach (KeyValuePair<string, int> item in keyValuePairs3){WriteLine("键:" + item.Key + " 值:" + item.Value);}WriteLine("------------------");ReadKey();

提示:1 KeyValuePair 和我们之前创建集合类中的 DictionaryEntry 很相似。2 集合中的键是唯一的,一旦重复添加则会引出异常。3 我引用了 using static System.Console; 为命名空间

指定 Dictionary<K,V> 比较键的自定义比较器

集合的键的判断方式为忽略大小写,所以当你添加一个为大写一个为小写时会报错 

 Dictionary<string, int> keyValuePairs = new Dictionary<string, int>(StringComparer.OrdinalIgnoreCase){["tow"] = 15,["Tow"] = 15//这里使用索引初始化器 似乎不会引起异常 我还不知道原因。};keyValuePairs.Add("sum", 20);keyValuePairs.Add("Sum", 20);ReadKey();

运行预览: 

907cc5bf0c42427c9dfb7f3444953094.png

三、定义泛型类型

1、定义泛型类

我们在类中定义泛型类只需要像如下代码定义就可以定义一个泛型类

/// <summary>
/// 定义泛型类
/// </summary>
/// <typeparam name="T"> T 泛型参数的标识符  但这不一定是用T表示 也可以使用别的代表 只需要符合C#命名规则就可以。也可以定义多个类型参数 </typeparam>
public class Student<T, T2, T3>
{//定义的类型参数 T 
}
public class Teacher<T, T2, T3>
{//定义的类型参数 T  T2  T3
}

示例解释:

类型“T”是一个泛型参数的标识符。“T”本身并不代表某个特定的类型,而是一个占位符,当你实际使用到这个类时,你可以为“T”提供具体 的一个类类型。

        定义泛型类成员,我们可以把刚刚定义的类型 定义为我们的属性、变量的类型,方法等成员的返回值。或者方法的参数

/// <summary>
/// 定义泛型类
/// </summary>
/// <typeparam name="T"> T 泛型参数的标识符   </typeparam>
public class Student<T>
{//定义T 类型成员private T _first;//定义T 类型属性public T First { get; set; }private T _second;//定义返回T类型的方法public T GetSecond() => _second;public Student(T first){//定义T类型的变量T firsts = first;_first = firsts;}
}

        注意我们不能假定“T” 为某种类型,如下代码是不可行的。(我们将上一个“Student”类的构造函数修改一下)

  public Student(T first){// 这里尝试使用 new T() 可能导致编译错误,因为编译器无法保证 T 是引用类型或有默认的无参构造函数。// 泛型类型 T 的行为和特性不确定,因此这种构造方法可能会在某些情况下失败。first = new T();}

常见的泛型中的方法:获取泛型参数的标识符类型

using static System.Console;
public class Program
{static void Main(){Student<string, int, double> student = new Student<string, int, double>();WriteLine(student.GetAllTypeAsString());ReadKey();}
}public class Student<T, T2, T3>
{public string GetAllTypeAsString(){return "T1:" + typeof(T) + "\nT2:" + typeof(T2) + "\nT3:" + typeof(T3);}
}

注意:我们在比较,泛型类提供的类型值和null 时,只能使用运算符== 或 != ,如下代码

public bool Compare( T t1,T2 t2)
{if (t1 != null && t2 != null){return true;}return false;
}

示例解释:

在方法 Compare 中,使用 t1 != null && t2 != null 这样的方式来比较两个泛型类型的值是否为 null,这样的比较并不适用于所有类型。泛型类型并不一定要求支持 null。例如,如果 TT2 是值类型(例如 intdouble 等),那么它们就不能赋值为 null。在这种情况下,你的比较会导致编译错误或者抛出异常。常见的就是一直返回true,因为值类型不会等于 Null,除非你是可空类型。

扩展


如果你想要更可靠地检查泛型类型是否为 null,可以考虑使用 EqualityComparer<T>.Default 类来进行比较 我修改一下比较代码

public class Student<T, T2>
{//比较public bool Compare(T t1, T2 t2){// 检查 t1 和 t2 是否为 nullbool t1IsNull = EqualityComparer<T>.Default.Equals(t1, default(T));bool t2IsNull = EqualityComparer<T2>.Default.Equals(t2, default(T2));return t1IsNull && t2IsNull;}
}

1.1 Default关键字

default关键字可以获取当前数据类型的默认数据。

举个例子:

public class Student<T, T2>
{public Student(T t1) => t1 = default(T); 
}

示例解释:

 t1 = null;//不能写等于 null 或者是0 因为无法假定t2为一种类型
  对于引用类型 T,t1 = null 是合法的,表示空值。
  对于值类型 T,t1 = default(T) 可以获得其默认值,例如数值类型为 0,布尔类型为 false。 

1.2 约束类型

        泛型可以对我们之前在泛型类中定义的泛型参数的标识符进行一个约束。泛型参数的标识符可以简称为类型参数。使用关键字 Where来实现约束类型参数。

public class Student<T, T2> where T : new()
{
}
public class Student2<T, T2> where T : Constraint, new()  
{
}
public class Student3<T, T2> where T : class where T2 : struct
{
}
public class Student4<T, T2> : CollectionBase where T : class where T2 : struct
{
}
public class Student5<T, T2>  where T : class where T2 : T
{
}
public class Student6<T, T2>  where T : T2 where T2 : T
{
}

示例解释: 

  1. 使用关键字 Where来实现
  2. 多个约束用逗号来区分开
  3. 可以使用多个where语句
  4. 约束必须出现在继承说明符的后面
  5. 可以把其它的类型参数的约束用作自身的类型约束
  6. 类型参数的约束不能循环 (”Student6“类 编译器会报错)

接下来我提供一个约束的表格,表达各种约束的含义

约束定义注意
Struct类型必须是值类型
Class类型必须是引用类型
interface类型必须是接口,或实现了接口提供某接口
new()类型必须有一个公共的无参构造函数必须是类型约束的最后一个约束
base-Class类型必须是某类的基类,或者继承自基类提供某类名

1.3 从泛型类中继承

1 约束在继承中对未约束的类型进行约束:如果一个基类使用了某种约束,比如 where T : class,那么派生类可以进一步添加额外的约束条件,对未被约束的类型参数进行约束。

举个例子:

class Base<T> where T : class { }class Derived<T> : Base<T> where T : IEnumerable { }

示例解释:

在这个例子中,Base<T> 有一个约束 where T : class,而 Derived<T> 继承自 Base<T> 并添加了一个额外的约束 where T : IEnumerable,这样就对 T 进行了进一步的约束。 

2 基类约束的继承性:如果基类对某个类型参数施加了约束,那么派生类中对于相同类型参数的约束至少要与基类的约束相同或更严格。

举个例子:

class Base<T> where T : class { }// 这是合法的,因为派生类的约束至少与基类相同
class Derived<T> : Base<T> where T : class { }// 这是不合法的,因为派生类的约束比基类宽松了
class Derived<T> : Base<T> where T : struct { }

3 提供类型的要求:如果继承了一个泛型类型,派生类必须提供类型参数。

举个例子:

class Base<T> { }// 这是不合法的,因为没有提供类型参数
class Derived : Base { }// 这是合法的,因为提供了类型参数
class Derived<T> : Base<T> { }

        这些规则帮助在派生类中维持和扩展对泛型类型参数的约束,并确保继承关系中的类型参数满足基类约束。 

提示:

        如果给泛型类型提供了参数,我们称呼该类型为“关闭的”,相反则是“打开的” 如lsit<Student> list<T>

1.4 泛型运算符

        C# 中的泛型运算符允许你对泛型类型参数进行某些操作,比如执行算术操作、逻辑比较等。这些运算符可以使用在泛型类、泛型结构体、以及泛型方法中。

public class MathOperations<T>
{public T Add(T a, T b){return (dynamic)a + (dynamic)b;}public T Subtract(T a, T b){return (dynamic)a - (dynamic)b;}// 其他算术运算符方法...
}

示例解释:

        在这个示例中,MathOperations<T> 类演示了使用泛型类型参数 T 进行加法和减法运算。通过使用 dynamic 关键字,可以允许在编译时确定运算符的实际操作。 

public class ComparisonOperations<T>
{public bool AreEqual(T a, T b){return EqualityComparer<T>.Default.Equals(a, b);}// 其他逻辑运算符方法...
}

示例解释:

        这个示例展示了如何使用 EqualityComparer<T> 类进行比较运算。这个类允许你比较泛型类型的值,检查它们是否相等。 

public class GenericOperator<T>
{public static T operator +(GenericOperator<T> a, GenericOperator<T> b){// 实现泛型类型的加法运算符重载// ...}// 其他运算符重载方法...
}

示例解释:

        这个示例展示了如何在泛型类中重载运算符。通过在泛型类中定义运算符的重载,可以让泛型类型支持各种自定义的操作。 

 1.5 泛型结构

泛型结构的定义与泛型类的定义相同。

using static System.Console;
public class Program
{static void Main(){MathStruct<double, int> mathStruct = new MathStruct<double, int>(100);mathStruct.MyProperty = 1;WriteLine(mathStruct.GetSum());ReadKey();}
}
public struct MathStruct<T23, T24> where T23 : struct where T24 : struct
{private T23 myVar;public T23 MyProperty{get { return myVar; }set { myVar = value; }}private T24 sum;//定义返回类型参数值的方法public T24 GetSum() => sum;public MathStruct(T24 t24){sum = t24;myVar = default;}
}

输出结果:100 

2、定义泛型接口

public interface IMyinterface<T>
{T GetValue();T MyProperty { get; set; }
}

3、定义泛型方法

泛型方法使得你能够在方法内部使用不特定类型的数据,提高了代码的重用性和灵活性。

 public static T convert<T>(object s) where T : struct{if (s is T){return (T)s;}return default;}

示例解释:

   if (s is T) 尝试检查对象 s 是否可以转换为类型 T。如果可以,就执行强制类型转换 (T)s 并返回转换后的值。否则,将返回 default(T),即 T 类型的默认值。 

4、定义泛型委托

举个例子:

public delegate void Mydelegate<T, T2>(T t, T2 t2) where T : T2 where T2 : class;

 示例解释:

  创建了一个委托无返回类型,有两个类型参数。分别为"T" "T2",对"T"的类型参数的约束是与"T2"的约束一致,"T2" 的约束则为引用类型

四、变体

1、协变

        协变,对于接口定义,协变类型参数只能用作方法的返回值或属性的Get访问器。使用关键字out。个人理解:可以将子类对象赋值给父类对象。

举个列子:

using System;namespace ConsoleApp2
{internal class Program{static void Main(string[] args){IMyInterface<BaseClass> interfaceInstance = new Student<DerivedClass>();BaseClass baseInstance = interfaceInstance.GetBase();}}interface IMyInterface<out T>{T GetBase();}class BaseClass{}class DerivedClass : BaseClass{}class Student<T> : IMyInterface<T>{public T GetBase(){return default;}}
}

示例解释:

         在 Main 方法中,我们创建了一个 Student<DerivedClass> 类的实例,并将其赋值给了 IMyInterface<BaseClass> 类型的变量 interfaceInstance。这里可以这么做是因为 Student<DerivedClass> 类型实现了 IMyInterface<DerivedClass> 接口,而由于协变的作用,IMyInterface<DerivedClass>IMyInterface<BaseClass> 的子类型,所以可以将 Student<DerivedClass> 类型赋值给 IMyInterface<BaseClass> 类型的变量。

总的来说,这段代码通过使用协变实现了将一个派生类型 Student<DerivedClass> 赋值给一个基类类型 IMyInterface<BaseClass> 的变量,允许在接口中使用更具体的类型。

  

2、抗变

        协变,对于接口定义,协变类型参数只能用作方法的参数,不能用作返回值。使用关键字in。个人理解:可以将父类对象赋值给子类对象。

举个例子:

using System;namespace ConsoleApp2
{internal class Program{static void Main(string[] args){IMyInterface<DerivedClass> interfaceInstance = new Student<BaseClass>();interfaceInstance.GetBase(new DerivedClass());}}interface IMyInterface<in T>{void GetBase(T s);}class BaseClass{}class DerivedClass : BaseClass{}class Student<T> : IMyInterface<T>{public void GetBase(T s){// 在实际场景中可能有不同的实现}}
}

示例解释:

        在 Main 方法中,创建了一个 Student<BaseClass> 类的实例,并将其赋值给了 IMyInterface<DerivedClass> 类型的变量 interfaceInstance。这里可以这么做是因为 Student<BaseClass> 类型实现了 IMyInterface<BaseClass> 接口,由于逆变的作用,IMyInterface<BaseClass>IMyInterface<DerivedClass> 的父类型,所以可以将 Student<BaseClass> 类型赋值给 IMyInterface<DerivedClass> 类型的变量。

然后,调用了 GetBase 方法并传入了 DerivedClass 类型的参数,这是因为 IMyInterface<DerivedClass> 接口的实现需要一个 DerivedClass 类型的参数。

实际使用中,可以看一下下面这段代码:

using System.Collections.Generic;
using System;class Program
{static void Main(string[] args){B bInstance = new B("lsonsgsstsaos");// 协变IContravariant<B> contravariantB = bInstance;IContravariant<A> contravariantA = contravariantB;// 逆变ICovariant<B> covariantB = bInstance;ICovariant<C> covariantC = covariantB;List<B> list = new List<B>(){{ new B("She") },{ new C("Student") },{ new B("from") }};list.Sort(OrderByStringLength);LoopThrough(list);Console.ReadKey();}private static int OrderByStringLength(B x, B y) => x.Name.Length.CompareTo(y.Name.Length);public static void LoopThrough(IEnumerable<A> enumerable){foreach (var item in enumerable){Console.WriteLine(item.Name);}}
}public class Meto
{}public abstract class A
{public abstract string Name { get; set; }
}class B : A, IContravariant<B>, ICovariant<B>
{public B(string name){this.Name = name;}public override string Name { get; set; }
}class C : B
{public C(string name) : base(name){}
}interface IContravariant<out T>
{}
interface ICovariant<in T>
{}

示例解释:   

        这段代码涉及到了协变和逆变,同时展示了类的继承和接口的使用。

  1. 协变和逆变的使用

    • 协变:IContravariant<out T> 接口使用了协变关键字 out,并在 Main 方法中展示了 IContravariant<B> 类型可以赋值给 IContravariant<A> 类型的示例。
    • 逆变:ICovariant<in T> 接口使用了逆变关键字 in,在 Main 方法中展示了 ICovariant<B> 类型可以赋值给 ICovariant<C> 类型的示例。
  2. 类和接口的关系

    • A 是一个抽象类,定义了一个抽象属性 Name
    • B 类继承自 A 类,并实现了 IContravariant<B>ICovariant<B> 接口。
    • C 类继承自 B 类。
  3. 方法和排序操作

    • OrderByStringLength 方法根据字符串长度对 B 类的实例进行排序。
    • LoopThrough 方法接受 IEnumerable<A> 类型的参数,并对其进行枚举操作。

总体来说,这段代码演示了协变和逆变在接口中的使用,以及类和接口之间的继承关系。同时也展示了泛型列表的排序和对列表的枚举操作。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/265382.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

dockerfile:创建镜像的方式,船舰自定义的镜像

dockerfile&#xff1a;创建镜像的方式&#xff0c;船舰自定义的镜像 包括配置文件&#xff0c;挂载点&#xff0c;对外暴露的端口&#xff0c;设置环境变量 docker创建镜像的方式 1、基于已有镜像进行创建。 根据官方提供的镜像源&#xff0c;创建镜像&#xff0c;然后拉起…

安装Nacos2.2.3集群

目录 一、传统方式安装 二、Docker安装 一、传统方式安装 1、配置jdk环境 vi /etc/profile JAVA_HOME/usr/local/java JRE_HOME/usr/local/java/jre CLASSPATH.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$JRE_HOME/lib PATH$JAVA_HOME/bin:$PATH export PATH JAVA_…

Vue3: 给表格多个字段添加排序功能

问题 在Vue3项目中&#xff0c;使用element-plus的表格组件绘制表格后&#xff0c;需要令表格的多个字段可以进行选择排序&#xff08;选择升序或者降序&#xff09;但是排序功能好像有时候会出错&#xff0c;需要排序的字段多了之后&#xff0c;排序功能有时候会不起作用 解…

酷滴科技出席浦发银行第七届国际金融科技创新大赛

12月7日&#xff0c;浦发银行全球金融科技创新大赛在上海展开决赛。本届大会以“科技金融&#xff0c;激发创新力量”为主题&#xff0c;聚焦金融行业数字化转型过程中的痛点与难点&#xff0c;旨在探讨新时代下金融科技的新角色、新机遇以及新挑战。酷滴科技CEO张沈分享了酷滴…

7+共病+PPI分析+转录调控,纯生信靠这个思路也能拿7+

今天给同学们分享一篇生信文章“Exploring the Pathogenesis of Psoriasis Complicated With Atherosclerosis via Microarray Data Analysis”&#xff0c;这篇文章发表在Front Immunol期刊上&#xff0c;影响因子为7.3。 结果解读&#xff1a; 差异表达基因的鉴定 该研究的…

selenium库的使用

来都来了给我点个赞收藏一下再走呗&#x1f339;&#x1f339;&#x1f339;&#x1f339;&#x1f339; 目录 一、下载需要用到的python库selenium 二、selenium的基本使用 1.在python代码引入库 2.打开浏览器 3.元素定位 1&#xff09;通过id定位 2&#xff09;通过标…

centos 7 卸载图形化界面步骤记录

centos7 服务器操作系统&#xff0c;挺小一配置&#xff0c;装了图形化界面&#xff0c;现在运行程序的时候跑不动了&#xff0c;我想这图形界面也没啥用&#xff0c;卸载了算了&#xff01; 卸载步骤 yum grouplist 查询已经安装的组件 可以看到 图形化界面 等是以分组存在的…

docker Compose-网络设置

目录 一、概述 二、容器网络模型(了解) CNM主要有三部分组成 CNM驱动接口 Docker内置网络驱动 三、Docker Compose-网络设置二 一、概述 二、使用 links 三、自定义网络 四、配置默认网络 五、已存在的网络 一、概述 随着微服务的事件&#xff0c;应用的越来越多&a…

详解http请求头,响应头以及在实际开发中

HTTP (Hypertext Transfer Protocol) 协议是一种用于传输超文本的标准协议&#xff0c;它是 Web 通信的基础。HTTP 协议是无状态的&#xff0c;即每次请求是相互独立的&#xff0c;服务器不会记住上一次请求的信息。HTTP 协议采用客户端-服务器模式&#xff0c;客户端发起请求&…

深入探讨JavaScript高阶编程技巧:突破技能瓶颈的高级实践

大家有关于JavaScript知识点不知道可以去 &#x1f389;博客主页&#xff1a;阿猫的故乡 &#x1f389;系列专栏&#xff1a;JavaScript专题栏 &#x1f389;欢迎关注&#xff1a;&#x1f44d;点赞&#x1f64c;收藏✍️留言 目录 学习目标&#xff1a; 学习内容&#xff1a…

ABeam Recruiting | ABeam旗下德硕管理咨询(深圳)最新社招岗位信息

ABeam Recruiting 职等你来 POSITION 招聘岗位 1 SAP项目经理 &#xff08;FICO/PP/MM/SD模块背景&#xff09; 职责描述 1.PD&Management&#xff1a;负责项目PD和落地实施管理 2.Business&#xff1a;熟悉了解业务需求、判断并合理地将业务需求转化成信息技术需求…

CMake是什么

文章目录 一.什么是CMake二.CMake安装三.CMake一个HelloWord-的语法介绍3.1 PROJECT关键字3.2 SET关键字3.3 MESSAGE关键字3.4 ADD_EXECUTABLE关键字3.5 include_directories关键字3.6 aux_source_directory 四.语法的基本原则4.1 语法注意事项 五.内部构建和外部构建5.1 外部构…