C#基础篇——泛型

前言

在开发编程中,咱们常常会遇到功能很是类似的功能模块,只是他们的处理的数据不同,因此咱们会分别采用多个方法来处理不一样的数据类型。可是这个时候,咱们就会想一个问题,有没有办法实现利用同一个方法来传递不一样种类型的参数呢?编程

这个时候,泛型也就因运而生,专门来解决这个问题的。c#

泛型是在C#2.0就推出的一个新语法,由框架升级提供的功能。设计模式

说明

泛型经过参数化类型实如今同一份代码上操做多种数据类型。例如使用泛型的类型参数T,定义一个类Stack 数组

能够用Stack 、Stack 或者Stack 实例化它,从而使类Stack能够处理int、string、Person类型数据。这样能够避免运行时类型转换或封箱操做的代价和风险。泛型提醒的是将具体的东西模糊化。 缓存

同时使用泛型类型能够最大限度地重用代码、保护类型安全以及提升性能。安全

能够建立:泛型接口泛型类泛型方法泛型事件泛型委托框架

开始

泛型类

泛型类封装不特定于特定数据类型的操做。 泛型类最多见用法是用于连接列表、哈希表、堆栈、队列和树等集合。 不管存储数据的类型如何,添加项和从集合删除项等操做的执行方式基本相同。ide

static void Main(string[] args)
    {

        // T是int类型
        GenericClass<int> genericInt = new GenericClass<int>();
        genericInt._T = 123;
        // T是string类型
        GenericClass<string> genericString = new GenericClass<string>();
        genericString._T = "123";

    }

新建一个GenericClass类函数

/// <summary>
    /// 泛型类
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public class GenericClass<T>
    {
        public T _T;
    }

泛型方法

泛型方法是经过类型参数声明的方法, 解决用一个方法,知足不一样参数类型性能

static void Main(string[] args)
    {
        #region 泛型方法
        Console.WriteLine("************Generic**************");
        int iValue = 123;
        string sValue = "456";
        DateTime dtValue = DateTime.Now;
        object oValue = "MrValue";
        GenericMethod.Show<int>(iValue);//须要指定类型参数
        //GenericMethod.Show<string>(iValue);//必须吻合
        GenericMethod.Show(iValue);//能省略,自动推算
        GenericMethod.Show<string>(sValue);
        GenericMethod.Show<DateTime>(dtValue);
        GenericMethod.Show<object>(oValue);
        #endregion

    }

新建一个GenericMethod

/// <summary>
/// 泛型方法
/// </summary>
public class GenericMethod
{
    /// <summary>
    /// 2.0推出的新语法
    /// 泛型方法解决用一个方法,知足不一样参数类型;作相同的事儿
    /// 没有写死参数类型,调用的时候才指定的类型
    /// 延迟声明:把参数类型的声明推迟到调用
    /// 推迟一切能够推迟的~~  延迟思想
    /// 不是语法糖,而是2.0由框架升级提供的功能
    /// 须要编译器支持+JIT支持
    /// </summary>
    /// <typeparam name="T">T/S 不要用关键字  也不要跟别的类型冲突 </typeparam>
    /// <param name="tParameter"></param>
    public static void Show<T>(T tParameter)
    {
        Console.WriteLine("This is {0},parameter={1},type={2}",
            typeof(GenericMethod), tParameter.GetType().Name, tParameter.ToString());
    }
}

泛型接口

为泛型集合类或表示集合中的项的泛型类定义接口一般颇有用处。在c#中,经过尖括号“<>”将类型参数括起来,表示泛型。声明泛型接口时,与声明通常接口的惟一区别是增长了一个 。通常来讲,声明泛型接口与声明非泛型接口遵循相同的规则。

泛型接口定义完成以后,就要定义此接口的子类。定义泛型接口的子类有如下两种方法。

(1)直接在子类后声明泛型。

(2)在子类实现的接口中明确的给出泛型类型。

static void Main(string[] args)
    {
        #region 泛型接口
        CommonInterface commonInterface = new CommonInterface();
        commonInterface.GetT("123");
        #endregion
    }

新建GenericInterface.cs类文件

/// <summary>
        /// 泛型类
        /// </summary>
        /// <typeparam name="T"></typeparam>
        public class GenericClass<T>
        {
            public T _T;
        }

        /// <summary>
        /// 泛型接口
        /// </summary>
        public interface IGenericInterface<T>
        {
            //泛型类型的返回值
            T GetT(T t);
        }


        /// <summary>
        /// 使用泛型的时候必须指定具体类型,
        /// 这里的具体类型是int
        /// </summary>
        public class CommonClass : GenericClass<int>
        {

        }

        /// <summary>
        /// 必须指定具体类型
        /// </summary>
        public class CommonInterface : IGenericInterface<string>
        {
            public string GetT(string t)
            {
                return t;
            }
        }

        /// <summary>
        /// 子类也是泛型的,继承的时候能够不指定具体类型
        /// </summary>
        /// <typeparam name="T"></typeparam>
        public class CommonClassChild<T> : GenericClass<T>
        {

        }

泛型委托

泛型委托主要是想讲一下Action 和Func 两个委托,由于这两个在Linq中是常常见到的。

Action 只能委托必须是无返回值的方法

Fun 只是委托必须有返回值的方法

无论是否是泛型委托,只要是委托委托那能用Lamdba表达式,由于无论Lamdba表达式仍是匿名函数其实都是将函数变量化。

下面简单的来作的demo说下两个的用法,这个会了基本linq会了一半了。

static void Main(string[] args)
    {
        #region 泛型委托
        Action<string> action = s => {
            Console.WriteLine(s);
        };
        action("i3yuan");
        Func<int, int, int> func = (int a, int b) => {
            return a + b;
        };
        Console.WriteLine("sum:{0}", func(1,1));
        Console.ReadLine();
        #endregion
    }

上面其实都是将函数作为变量,这也是委托的思想。action是实例化了一个只有一个字符串参数没有返回值得函数变量。func是实例化了一个有两个int类型的参数返回值为int的函数变量。

能够看到经过Lamdba表达式和泛型的结合,算是又方便了开发者们,更加方便实用。

引入委托经常使用的另外一方式

不管是在类定义内仍是类定义外,委托能够定义本身的类型参数。引用泛型委托的代码能够指定类型参数来建立一个封闭构造类型,这和实例化泛型类或调用泛型方法同样,以下例所示:

public delegate void MyDelegate<T>(T item);
public void Notify(int i){}
//...
 
MyDelegate<int> m = new MyDelegate<int>(Notify);
 
C#2.0版有个新特性称为方法组转换(method group conversion),具体代理和泛型代理类型均可以使用。用方法组转换能够把上面一行写作简化语法:
MyDelegate<int> m = Notify;
 
在泛型类中定义的委托,能够与类的方法同样地使用泛型类的类型参数。
class Stack<T>
{
T[] items;
      int index
//...
public delegate void StackDelegate(T[] items);
}
 
引用委托的代码必需要指定所在类的类型参数,以下:
 
Stack<float> s = new Stack<float>();
Stack<float>.StackDelegate myDelegate = StackNotify;
 
 
泛型委托在定义基于典型设计模式的事件时特别有用。由于sender[JX2] ,而不再用与Object相互转换。
public void StackEventHandler<T,U>(T sender, U eventArgs);
class Stack<T>
{
    //…
    public class StackEventArgs : EventArgs{...}
    public event StackEventHandler<Stack<T>, StackEventArgs> stackEvent;
    protected virtual void OnStackChanged(StackEventArgs a)
    {
      stackEvent(this, a);
    }
}
class MyClass
{
  public static void HandleStackChange<T>(Stack<T> stack, StackEventArgs args){...};
}
Stack<double> s = new Stack<double>();
MyClass mc = new MyClass();
s.StackEventHandler += mc.HandleStackChange;

泛型约束

所谓的泛型约束,实际上就是约束的类型T。使T必须遵循必定的规则。好比T必须继承自某个类,或者T必须实现某个接口等等。那么怎么给泛型指定约束?其实也很简单,只须要where关键字,加上约束的条件。

定义一个People类,里面有属性和方法:

public interface ISports
    {
        void Pingpang();
    }
    public interface IWork
    {
        void Work();
    }
    public class People
    {
        public int Id { get; set; }
        public string Name { get; set; }
    
        public void Hi()
        {
            Console.WriteLine("Hi");
        }
    
    }
    public class Chinese : People, ISports, IWork
    {
        public void Tradition()
        {
            Console.WriteLine("仁义礼智信,温良恭俭让");
        }
        public void SayHi()
        {
            Console.WriteLine("吃了么?");
        }
    
        public void Pingpang()
        {
            Console.WriteLine("打乒乓球...");
        }
    
        public void Work()
        {
            throw new NotImplementedException();
        }
    } 
    public class Hubei : Chinese
    {
        public Hubei(int version)
        { }
    
        public string Changjiang { get; set; }
        public void Majiang()
        {
            Console.WriteLine("打麻将啦。。");
        }
    }
    public class Japanese : ISports
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public void Hi()
        {
            Console.WriteLine("Hi");
        }
        public void Pingpang()
        {
            Console.WriteLine("打乒乓球...");
        }
    }

打印方法

/// <summary>
    /// 打印个object值
    /// 1 object类型是一切类型的父类
    /// 2 经过继承,子类拥有父类的一切属性和行为;任何父类出现的地方,均可以用子类来代替
    /// object引用类型  加入传个值类型int  会有装箱拆箱  性能损失
    /// 类型不安全
    /// </summary>
    /// <param name="oParameter"></param>
    public static void ShowObject(object oParameter)
    {
        Console.WriteLine("This is {0},parameter={1},type={2}",
            typeof(Constraint), oParameter.GetType().Name, oParameter);

        Console.WriteLine($"{((People)oParameter).Id}_{((People)oParameter).Name}");

    }

在main方法中

static void Main(string[] args)
    {
        #region  Constraint 接口约束
        Console.WriteLine("************Constraint*****************");
        {
            People people = new People()
            {
                Id = 123,
                Name = "走本身的路"
            };
            Chinese chinese = new Chinese()
            {
                Id = 234,
                Name = "晴天"
            };
            Hubei hubei = new Hubei(123)
            {
                Id = 345,
                Name = "流年"
            };
            Japanese japanese = new Japanese()
            {
                Id = 7654,
                Name = "i3yuan"//
            };
            CommonMethod.ShowObject(people);
            CommonMethod.ShowObject(chinese);
            CommonMethod.ShowObject(hubei);
            CommonMethod.ShowObject(japanese);
  
            Console.ReadLine();
        }
        #endregion
    }

泛型约束总共有五种。

约束 说明
T:结构 类型参数必须是值类型
T:类 类型参数必须是引用类型;这一点也适用于任何类、接口、委托或数组类型。
T:new() 类型参数必须具备无参数的公共构造函数。 当与其余约束一块儿使用时,new() 约束必须最后指定。
T:<基类名> 类型参数必须是指定的基类或派生自指定的基类。
T:<接口名称> 类型参数必须是指定的接口或实现指定的接口。 能够指定多个接口约束。 约束接口也能够是泛型的。

一、基类约束

上面打印的方法约束T类型必须是People类型。

///


/// 基类约束:约束T必须是People类型或者是People的子类
/// 1 可使用基类的一切属性方法---权利
/// 2 强制保证T必定是People或者People的子类---义务

/// <summary>
        /// 
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="tParameter"></param>
        public static void Show<T>(T tParameter) where T : People
        {
            Console.WriteLine($"{tParameter.Id}_{tParameter.Name}");
            tParameter.Hi();
        }

注意:

基类约束时,基类不能是密封类,即不能是sealed类。sealed类表示该类不能被继承,在这里用做约束就无任何意义,由于sealed类没有子类。

二、接口约束

/// <summary>
        /// 接口约束
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="t"></param>
        /// <returns></returns>
        public static T Get<T>(T t) where T : ISports
        {
            t.Pingpang();
            return t;
        }

三、引用类型约束 class

引用类型约束保证T必定是引用类型的。

/// 引用类型约束
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="t"></param>
        /// <returns></returns>
        public static T Get<T>(T t) where T : class
        {
            return t;
        }

四、值类型约束 struct

值类型约束保证T必定是值类型的。

/// 值类型类型约束
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="t"></param>
        /// <returns></returns>
        public static T Get<T>(T t) where T : struct
        {
            return t;
        }

五、无参数构造函数约束 new()

/// new()约束
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="t"></param>
        /// <returns></returns>
        public static T Get<T>(T t) where T : new()
        {
            return t;
        }

泛型约束也能够同时约束多个,例如:

/// <summary>
        ///  泛型:不一样的参数类型都能进来;任何类型都能过来,你知道我是谁?
        /// 没有约束,也就没有自由
        ///  泛型约束--基类约束(不能是sealed):
        /// 1 可使用基类的一切属性方法---权利
        /// 2  强制保证T必定是People或者People的子类---义务
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="tParameter"></param>
        public static void Show<T>(T tParameter)
        where T : People, ISports, IWork, new()
        {
            Console.WriteLine($"{tParameter.Id}_{tParameter.Name}");
            tParameter.Hi();
            tParameter.Pingpang();
            tParameter.Work();
        }

注意:有多个泛型约束时,new()约束必定是在最后。

泛型的协变和逆变

public class Animal
    {
        public int Id { get; set; }
    }

    public class Cat : Animal
    {
        public string Name { get; set; }
    }
static void Main(string[] args)
 {
    #region 协变和逆变

    // 直接声明Animal类
    Animal animal = new Animal();
    // 直接声明Cat类
    Cat cat = new Cat();
    // 声明子类对象指向父类
    Animal animal2 = new Cat();
    // 声明Animal类的集合
    List<Animal> listAnimal = new List<Animal>();
    // 声明Cat类的集合
    List<Cat> listCat = new List<Cat>();

    #endregion
 }

那么问题来了:下面的一句代码是否是正确的呢?

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

可能有人会认为是正确的:由于一只Cat属于Animal,那么一群Cat也应该属于Animal啊。可是实际上这样声明是错误的:由于List 和List 之间没有父子关系。

image-2020053023015097

这时就能够用到协变和逆变了。

1 // 协变
2 IEnumerable<Animal> List1 = new List<Animal>();
3 IEnumerable<Animal> List2 = new List<Cat>();

F12查看定义:

能够看到,在泛型接口的T前面有一个out关键字修饰,并且T只能是返回值类型,不能做为参数类型,这就是协变。使用了协变之后,左边声明的是基类,右边能够声明基类或者基类的子类。

协变除了能够用在接口上面,也能够用在委托上面:

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

除了使用.NET框架定义好的觉得,咱们还能够自定义协变,例如:

/// <summary>
    /// out 协变 只能是返回结果
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public interface ICustomerListOut<out T>
    {
        T Get();
    }

    public class CustomerListOut<T> : ICustomerListOut<T>
    {
        public T Get()
        {
            return default(T);
        }
    }

使用自定义的协变:

// 使用自定义协变
 ICustomerListOut<Animal> customerList1 = new CustomerListOut<Animal>();
 ICustomerListOut<Animal> customerList2 = new CustomerListOut<Cat>();

在来看看逆变。

在泛型接口的T前面有一个In关键字修饰,并且T只能方法参数,不能做为返回值类型,这就是逆变。请看下面的自定义逆变:

/// <summary>
    /// 逆变 只能是方法参数
    /// </summary>
    /// <typeparam name="T"></typeparam>
    public interface ICustomerListIn<in T>
    {
        void Show(T t);
    }

    public class CustomerListIn<T> : ICustomerListIn<T>
    {
        public void Show(T t)
        {
        }
    }

使用自定义逆变:

// 使用自定义逆变
 ICustomerListIn<Cat> customerListCat1 = new CustomerListIn<Cat>();
 ICustomerListIn<Cat> customerListCat2 = new CustomerListIn<Animal>();

协变和逆变也能够同时使用,看看下面的例子:

/// <summary>
    /// inT 逆变
    /// outT 协变
    /// </summary>
    /// <typeparam name="inT"></typeparam>
    /// <typeparam name="outT"></typeparam>
    public interface IMyList<in inT, out outT>
    {
        void Show(inT t);
        outT Get();
        outT Do(inT t);
    }

    public class MyList<T1, T2> : IMyList<T1, T2>
    {

        public void Show(T1 t)
        {
            Console.WriteLine(t.GetType().Name);
        }

        public T2 Get()
        {
            Console.WriteLine(typeof(T2).Name);
            return default(T2);
        }

        public T2 Do(T1 t)
        {
            Console.WriteLine(t.GetType().Name);
            Console.WriteLine(typeof(T2).Name);
            return default(T2);
        }
    }

使用:

IMyList<Cat, Animal> myList1 = new MyList<Cat, Animal>();
 IMyList<Cat, Animal> myList2 = new MyList<Cat, Cat>();//协变
 IMyList<Cat, Animal> myList3 = new MyList<Animal, Animal>();//逆变
 IMyList<Cat, Animal> myList4 = new MyList<Animal, Cat>();//逆变+协变

有关可变性的注意事项

  • 变化只适用于引用类型,由于不能直接从值类型派生其余类型
  • 显示变化使用in和out关键字只适用于委托和接口,不适用于类、结构和方法
  • 不包括in和out关键字的委托和接口类型参数叫作不变

泛型缓存

在前面咱们学习过,类中的静态类型不管实例化多少次,在内存中只会有一个。静态构造函数只会执行一次。在泛型类中,T类型不一样,每一个不一样的T类型,都会产生一个不一样的副本,因此会产生不一样的静态属性、不一样的静态构造函数,请看下面的例子:

public class GenericCache<T>
{
    static GenericCache()
    {
        Console.WriteLine("This is GenericCache 静态构造函数");
        _TypeTime = string.Format("{0}_{1}", typeof(T).FullName, DateTime.Now.ToString("yyyyMMddHHmmss.fff"));
    }

    private static string _TypeTime = "";

    public static string GetCache()
    {
        return _TypeTime;
    }
}
public class GenericCacheTest
{
    public static void Show()
    {
        for (int i = 0; i < 5; i++)
        {
            Console.WriteLine(GenericCache<int>.GetCache());
            Thread.Sleep(10);
            Console.WriteLine(GenericCache<long>.GetCache());
            Thread.Sleep(10);
            Console.WriteLine(GenericCache<DateTime>.GetCache());
            Thread.Sleep(10);
            Console.WriteLine(GenericCache<string>.GetCache());
            Thread.Sleep(10);
            Console.WriteLine(GenericCache<GenericCacheTest>.GetCache());
            Thread.Sleep(10);
        }
    }
}

Main()方法里面调用:

static void Main(string[] args)
 {
    #region 泛型缓存
	GenericCacheTest.Show();
    #endregion
 }

结果:

20200530232600809

从上面的截图中能够看出,泛型会为不一样的类型都建立一个副本,因此静态构造函数会执行5次。 并且每次静态属性的值都是同样的。利用泛型的这一特性,能够实现缓存。

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

注意

1.泛型代码中的 default 关键字

在泛型类和泛型方法中会出现的一个问题是,如何把缺省值赋给参数化类型,此时没法预先知道如下两点:

  • T将是值类型仍是引用类型

  • 若是T是值类型,那么T将是数值仍是结构

对于一个参数化类型T的变量t,仅当T是引用类型时,t = null语句才是合法的; t = 0只对数值的有效,而对结构则不行。这个问题的解决办法是用default关键字,它对引用类型返回空,对值类型的数值型返回零。而对于结构,它将返回结构每一个成员,并根据成员是值类型仍是引用类型,返回零或空。下面GenericList 类的例子显示了如何使用default关键字。

static void Main(string[] args)
    {
        #region 泛型代码默认关键字default
        // 使用非空的整数列表进行测试.
        GenericList<int> gll = new GenericList<int>();
        gll.AddNode(5);
        gll.AddNode(4);
        gll.AddNode(3);
        int intVal = gll.GetLast();
        // 下面一行显示5.
        Console.WriteLine(intVal);

        // 用一个空的整数列表进行测试.
        GenericList<int> gll2 = new GenericList<int>();
        intVal = gll2.GetLast();
        // 下面一行显示0.
        Console.WriteLine(intVal);

        // 使用非空字符串列表进行测试.
        GenericList<string> gll3 = new GenericList<string>();
        gll3.AddNode("five");
        gll3.AddNode("four");
        string sVal = gll3.GetLast();
        // 下面一行显示five.
        Console.WriteLine(sVal);

        // 使用一个空字符串列表进行测试.
        GenericList<string> gll4 = new GenericList<string>();
        sVal = gll4.GetLast();
        // 下面一行显示一条空白行.
        Console.WriteLine(sVal);
        #endregion
        Console.ReadKey();
    }
public class GenericList<T>
    {
        private class Node
        {
            // 每一个节点都有一个指向列表中的下一个节点的引用.
            public Node Next;
            // 每一个节点都有一个T类型的值.
            public T Data;
        }

        // 这个列表最初是空的.
        private Node head = null;

        // 在列表开始的时候添加一个节点,用t做为它的数据值.
        public void AddNode(T t)
        {
            Node newNode = new Node();
            newNode.Next = head;
            newNode.Data = t;
            head = newNode;
        }

        // 下面的方法返回存储在最后一个节点中的数据值列表. 若是列表是空的, 返回类型T的默认值.
        public T GetLast()
        {
            // 临时变量的值做为方法的值返回. 
            // 下面的声明初始化了临时的温度 
            // 类型T的默认值. 若是该列表为空返回默认值.
            T temp = default(T);

            Node current = head;
            while (current != null)
            {
                temp = current.Data;
                current = current.Next;
            }
            return temp;
        }
    }

2.泛型集合

一般状况下,建议您使用泛型集合,由于这样能够得到类型安全的直接优势而不须要从基集合类型派生并实现类型特定的成员。下面的泛型类型对应于现有的集合类型:

一、List 是对应于 ArrayList 的泛型类。
二、Dictionary 是对应于 Hashtable 的泛型类。
三、Collection 是对应于 CollectionBase 的泛型类。
四、ReadOnlyCollection 是对应于 ReadOnlyCollectionBase 的泛型类。
五、QueueStackSortedList 泛型类分别对应于与其同名的非泛型类。
六、LinkedList 是一个通用连接列表,它提供运算复杂度为 O(1) 的插入和移除操做。
七、SortedDictionary 是一个排序的字典,其插入和检索操做的运算复杂度为 O(log n),这使得它成为 SortedList 的十分有用的替代类型。
八、KeyedCollection 是介于列表和字典之间的混合类型,它提供了一种存储包含本身键的对象的方法。

总结

  1. 做为一个开发人员,当咱们程序代码有相同的逻辑,有多是方法、接口、类或者委托,只是某些参数类型不一样,咱们但愿代码能够通用、复用,甚至是说为了偷懒,也能够说是在不肯定类型的状况下,就应该考虑用泛型的思惟去实现。
  2. 在非泛型编程中,虽然全部的东西均可以做为Object传递,可是在传递的过程当中免不了要进行类型转换。而类型转换在运行时是不安全的。使用泛型编程将能够减小没必要要的类型转换,从而提升安全性。不只是值类型,引用类型也存在这样的问题,所以有必要的尽可能的去使用泛型集合。
  3. 在非泛型编程中,将简单类型做为Object传递时会引发装箱和拆箱的操做,这两个过程都是具备很大开销的。使用泛型编程就没必要进行装箱和拆箱操做了。

参考 文档 《C#图解教程》

注:搜索关注公众号【DotNet技术谷】--回复【C#图解】,可获取 C#图解教程文件

相关文章
相关标签/搜索