C#高级教程(三)看不懂捶我:泛型

本讲用许多代码示例介绍了 C# 语言当中的泛型,主要包括泛型类、接口、结构、委托和方法。

1. 为什么需要泛型?

在之前的教程中,我们使用的自定义类都是具有具体的类型。如果对类型进行抽象,该类型就是泛型,即一种广泛的类型。例如,我们有一个水果篮,水果篮中可以放不同种类的水果,例如苹果、香蕉、菠萝等等。这个水果篮可以放置的水果就是一种泛型,水果可以代指很多不同种类的水果。

为什么需要泛型?泛型的好处之一就是能够提高代码的复用性。例如,我们有一种栈数据结构,栈当中的类型可以是 intfloat等等,但很多操作都是相同的:入栈、出栈、获取栈大小,所不同的是数据类型的不同。利用泛型,我们只需要实现一套代码,而不需要针对不同的数据类型分别实现各自的一套栈代码。

在 C# 中,泛型又称为参数化类型。泛型可以作用在类、函数、结构、委托和接口,由此衍生出泛型类、泛型函数、泛型结构、泛型委托和泛型接口的概念。

2. 泛型类的定义

2.1 泛型类的定义

2.2 使用泛型类创建变量和实例

在使用泛型类创建变量时,可以使用关键字 var,让编译器根据 = 右边的类型自动推断变量的类型。

csharp 复制代码
using System;

class SomeClass<T1, T2>
{
    public T1 Property1 { get; set; }
    public T2 Property2 { get; set; }

    // Constructor
    public SomeClass(T1 property1, T2 property2)
    {
        Property1 = property1;
        Property2 = property2;
    }
}
class Program
{
    static void Main(string[] args)
    {
        SomeClass<int, float> sc = new SomeClass<int, float>(10, 3.14f);
        var sc2 = new SomeClass<string, bool>("Hello", true);  // 让编译器推断类型
        Console.WriteLine(sc.Property1);
        Console.WriteLine(sc.Property2);
        Console.WriteLine(sc2.Property1);
        Console.WriteLine(sc2.Property2);
    }
}

输出:

arduino 复制代码
10
3.14
Hello
true

需要注意的是,为具体类分配的内存是存储在堆上的。

3. 使用泛型类实现一个简单的栈

csharp 复制代码
using System;

namespace GenericStackExample
{
    // 定义一个泛型栈类  
    public class Stack<T>
    {
        // 使用数组来存储栈中的元素  
        private T[] _items;
        // 栈顶元素的索引(初始化为-1表示栈为空)  
        private int _top;
        // 栈的容量  
        private int _capacity;
        
        // 构造函数,初始化栈的容量  
        public Stack(int capacity = 10)
        {
            _capacity = capacity;
            _items = new T[capacity];
            _top = -1;
        }
        // 检查栈是否为空  
        public bool IsEmpty()
        {
            return _top == -1;
        }
        // 检查栈是否已满  
        public bool IsFull()
        {
            return _top == _capacity - 1;
        }
        // 入栈操作  
        public void Push(T item)
        {
            if (IsFull())
            {
                throw new InvalidOperationException("Stack is full.");
            }

            _items[++_top] = item;
        }
        // 出栈操作  
        public T Pop()
        {
            if (IsEmpty())
            {
                throw new InvalidOperationException("Stack is empty.");
            }

            return _items[_top--];
        }
        // 查看栈顶元素(不移除)  
        public T Peek()
        {
            if (IsEmpty())
            {
                throw new InvalidOperationException("Stack is empty.");
            }
            return _items[_top];
        }
        // 获取栈的大小(元素数量)  
        public int Size()
        {
            return _top + 1;
        }
    }
    class Program
    {
        static void Main(string[] args)
        {
            Stack<int> intStack = new Stack<int>(5); // 创建一个整型栈  
            // 入栈操作  
            intStack.Push(1);
            intStack.Push(2);
            intStack.Push(3);

            // 访问栈顶元素  
            Console.WriteLine($"栈顶元素是: {intStack.Peek()}");
            // 出栈操作  
            Console.WriteLine($"出栈元素: {intStack.Pop()}");
            // 获取栈的大小  
            Console.WriteLine($"栈的大小是: {intStack.Size()}");
            // 尝试在空栈上执行出栈操作以演示异常  
            try
            {
                intStack.Pop();
            }
            catch (InvalidOperationException ex)
            {
                Console.WriteLine(ex.Message);
            }
        }
    }
}

3.1 类型参数的约束

现在我们能够设计出泛型类,但是泛型类型它本身提供什么方法,我们是不知道的。例如下面的泛型类:

csharp 复制代码
class Simple<T>
{
	static public bool LessThan(T i1, T i2)
	{
		return i1 < i2;
	}
}
...

但不是所有类型 T 都实现了小于运算符,所以会报出错误: 为此,我们需要告诉编译器关于泛型类型的额外信息,让其知道参数可以接受哪些类型。这些额外的信息叫做约束(constrain)。没有任何约束的类型参数称为未绑定的类型参数(unbounded type parameter)。

3.2 Where 子句

约束使用 where 子句列出:

3.3 约束类型和次序

共有 5 种类型的约束,如下表所示:

约束类型 描述
某个具体的类名 只有这个类型的类或从它继承的类才能用作类型参数
class 任何引用类型,包括类、数组、委托和接口都可以用作类型参数
struct 任何值类型都可以用作类型参数
接口名 只有这个接口或实现这个接口的类型才能用作类型参数
new() 任何带有无参公共构造函数的类型都可以用作类型参数。这叫做构造函数约束

对不同类型的 where 约束可以以任何顺序列出。但是,where 子句内的约束必须遵循一定的顺序:

  • 最多只能具有一个主约束,有的话必须放到第一位;
  • 可以有任意多的接口名约束;
  • 如果存在构造函数约束,则必须放到最后。 下面是一些例子:
csharp 复制代码
class SortedList<S>
    where S: IComparable<S>
{ }

class LinkedList<M, N>
    where M: IComparable<M>
    where N: ICloneable
{ }

class MyDictionary<KeyType, ValueType>
    where KeyType: IComparable<KeyType>,
    new()
{ }

4. 泛型方法

泛型方法的定义如下:

泛型方法的使用:

csharp 复制代码
void DoStuff<T1, T2> (T1 t1, T2 t2)
{
    T1 someVar = t1;
    T2 otherVar = t2;
}

DoStuff<int, string>(10, "Hello");
DoStuff<int, long>(iVal, lVal);

当参数类型列表的类型和方法列表中的类型相同时,在使用泛型方法时可以省略对参数类型的指定,例如:

csharp 复制代码
public void MyMethod<T> (T val) {}
int myInt = 5;
MyMethod(myInt);

泛型方法的使用示例:

csharp 复制代码
class Simple
{
    static public void ReverseAndPrint<T>(T[] arr)
    {
        Array.Reverse(arr);
        foreach (var item in arr)
        {
            Console.Write("{0}, ", item.ToString());
            Console.WriteLine();
        }
    }
}

class Program
{
    static void Main()
    {
        // 创建整数、字符串、浮点型数组  
        int[] intArray = { 1, 2, 3, 4, 5 };
        string[] stringArray = { "hello", "world" };
        float[] floatArray = { 1.1f, 2.2f, 3.3f };

        // 调用泛型方法,反转并打印数组  
        Simple.ReverseAndPrint(intArray);
        Simple.ReverseAndPrint<int>(intArray);

        Simple.ReverseAndPrint(stringArray);
        Simple.ReverseAndPrint<string>(stringArray);

        Simple.ReverseAndPrint(floatArray);
        Simple.ReverseAndPrint<float>(floatArray);

    }
}

输出结果:

5. 泛型结构

泛型结构和泛型类的定义类似,直接给出例子。

csharp 复制代码
struct PieceOfData<T>
{
    public T _data { get; set; }
    public PieceOfData(T data)
    {
        _data = data;
    }
}

class Program
{
    static void Main()
    {
        PieceOfData<int> piece1 = new PieceOfData<int>(10);
        PieceOfData<string> piece2 = new PieceOfData<string>("Hello");

        Console.WriteLine(piece1._data);
        Console.WriteLine(piece2._data);
    }
}

6. 泛型委托

委托类型当中可以使用泛型的地方:

  • 返回类型
  • 类型参数
  • where子句

泛型委托的例子:

csharp 复制代码
delegate void MyDelegate<T>(T value);

class Simple
{
    static public void PrintString(string s)
    {
        Console.WriteLine(s);
    }

    static public void PrintUpperString(string s)
    {
        Console.WriteLine(s.ToUpper());
    }
}

class Prgram
{
    static void Main()
    {
        MyDelegate<string> d1 = new MyDelegate<string>(Simple.PrintString);
        d1 += Simple.PrintUpperString;

        d1("Hello");
    }
}

输出:

bash 复制代码
hello
HELLO

7. 泛型接口

csharp 复制代码
interface IMyIfc<T>
{
    T ReturnIt(T invalue);
}

class Simple: IMyIfc<int>, IMyIfc<string>
{
    // 因为 Simple 类实现了两个接口,所以它必须实现两个接口的相同方法。
    public int ReturnIt(int invalue)
    {
        return invalue;
    }

    public string ReturnIt(string invalue)
    {
        return invalue;
    }
}

class Program
{
    static void Main()
    {
        IMyIfc<int> intIfc = new Simple();
        IMyIfc<string> stringIfc = new Simple();

        Console.WriteLine(intIfc.ReturnIt(10));
        Console.WriteLine(stringIfc.ReturnIt("Hello"));
    }
}

本章小结:主要通过例子讲解了 C# 语言当中泛型{类、接口、结构、委托、方法}的用法。

各位道友,码字不易,记得一键三连啊。

相关推荐
IT规划师4 小时前
开源 - Ideal库 - 常用枚举扩展方法(一)
开源·c#·.net core·ideal库·枚举转换
NetX行者10 小时前
.NET 9震撼来袭:基于.NET 8的五大功能亮点,引领开发新潮流
开发语言·microsoft·c#·.netcore
张某布响丸辣10 小时前
HTTP状态码详解
java·网络·python·网络协议·http·c#
飞舞的哈哈11 小时前
C# 有趣的小程序—桌面精灵详细讲解
c#
Skyshin3413 小时前
C# IEnumerator,IEnumerable ,Iterator
开发语言·c#
ling1s13 小时前
C#核心(7)索引器
开发语言·c#
LKID体14 小时前
win32com库基于wps对Word文档的基础操作
c#·word·wps
金蝶软件小李14 小时前
vector和docker的区别?
开发语言·docker·c#
金蝶软件小李16 小时前
图像处理椒盐噪声
开发语言·图像处理·算法·计算机视觉·c#
小吴同学·17 小时前
(实战)WebApi第13讲:怎么把不同表里的东西,包括同一个表里面不同的列设置成不同的实体,所有的给整合到一起?【前端+后端】、前端中点击标签后在界面中显示
c#·.netcore·.net core