您的位置:首页 > 科技 > 能源 > 临邑网页设计_移动互联网开发技术题库_漳州网络推广_百度网站收录

临邑网页设计_移动互联网开发技术题库_漳州网络推广_百度网站收录

2025/5/9 12:46:09 来源:https://blog.csdn.net/qq_39847278/article/details/145769595  浏览:    关键词:临邑网页设计_移动互联网开发技术题库_漳州网络推广_百度网站收录
临邑网页设计_移动互联网开发技术题库_漳州网络推广_百度网站收录

总目录


前言

在 C# 编程中,代码的复用性和灵活性是至关重要的。

在传统编程方式中,若需处理不同数据类型的相似逻辑,往往需要为每个类型编写重复代码。例如,针对intstring的集合操作需分别实现,这不仅冗余,还可能导致类型安全隐患。

在C# 2.0引入泛型后,它彻底改变了开发者编写可复用代码的方式。C#泛型(Generics)通过延迟类型指定(或称 类型参数化)的机制,允许开发者编写可复用的类型安全代码,更通过消除装箱拆箱操作显著优化了性能。接下来,我们就来深入探讨一下 C# 泛型的使用吧。


一、什么是泛型

1. 基本概念

  • 泛型(Generics)是一种编程范式,它允许我们在定义类、方法或接口时,使用占位符(类型参数)代替具体的类型。在实际使用时,这些占位符会被具体的类型替换,从而实现类型安全的代码复用。
  • 通过使用泛型,我们可以编写适用于多种数据类型的代码,而无需为每种类型单独写代码,这不仅提高了代码的复用性,还增强了类型安全性和效率。

2. 泛型的优点

  • 类型安全:由于泛型代码在编译时会进行类型检查,因此减少了运行时出现错误的可能性。
  • 性能提升:泛型在运行时会生成特定类型的代码,避免了装箱和拆箱操作,提高了性能。
  • 代码复用:通过泛型,我们可以编写多种数据类型通用的类和方法,减少重复代码。
  • 可读性增强:泛型代码更清晰,意图更明确。

3. 痛点场景

示例:要求实现输入int ,string,datetime类型的值的时候,打印出对应的类型和值

	public class CommonMethod{//打印int的数据类型和值public static void ShowInt(int a){Console.WriteLine($"result:type={a.GetType().Name},value={a}");}//打印string的数据类型和值public static void ShowString(string s){Console.WriteLine($"result:type={s.GetType().Name},value={s}");}//打印DateTime 的数据类型和值public static void ShowDateTime(DateTime dt){Console.WriteLine($"result:type={dt.GetType().Name},value={dt}");}}

以上示例中除了传入参数的数据类型不同,其余的处理逻辑相同,明显代码没有得到复用,于是简化代码如下:

        // 打印输入参数的数据类型和值public static void ShowObject(object o){Console.WriteLine($"result:type={o.GetType().Name},value={o}");}

示例中,直接使用object 完成了代码的复用,让代码变得更加简洁通用,但是在这个过程中存在数据类型转化,也就涉及到了装箱和拆箱,严重的影响程序的性能。

那么现在能不能找一个 既能满足 代码复用的需求 又能避免装箱和拆箱带来性能损耗的方法呢?
有,那就是使用泛型,使用泛型优化示例代码:

        //打印输入参数的数据类型和值public static void ShowResult<T>(T t){Console.WriteLine($"result:type={t.GetType().Name},value={t}");}

二、如何使用泛型?

1. 类型参数

  • 类型参数:是指在定义类、接口或方法时使用的占位符,代表实际使用时指定的数据类型。
    • 例如,在List<T>中,T就是类型参数。
    • 类型参数 可以代表任何类型,也可识别任何类型
  • 类型参数命名规范:推荐使用TTKeyTValue等有意义的类型参数名
    • 泛型参数一般用T表示,但是不代表不可以使用别的代表,也可使用V、K等自定义的名称,但是推荐命名的时候使用T或者T开头
  • 类型参数数量:数量不限,业务中需要几个类型参数,就设置几个类型参数,不过建议类型参数不要过多

使用一对尖括号 + 类型参数 T ,如 MyGenericClass<T>List<T>这种形式来定义泛型对象。

2. 泛型类

泛型类是最常见的泛型形式。它允许我们定义一个类,其行为可以独立于具体的数据类型。

1)泛型类

  • 定义泛型类的基本语法如下:
    • 这里T是类型参数,代表任何数据类型。创建对象时,需要指定实际的数据类型。
	public class Box<T>{private T _item;public void Set(T item){_item = item;}public T Get(){return _item;}}

在这个例子中,Box<T> 是一个泛型类,T 是类型参数。我们可以通过指定具体的类型来实例化这个类:

  • 使用泛型类
	Box<int> intBox = new Box<int>();intBox.Set(42);Console.WriteLine(intBox.Get()); // 输出:42Box<string> stringBox = new Box<string>();stringBox.Set("Hello, World!");Console.WriteLine(stringBox.Get()); // 输出:Hello, World!

实例化 泛型对象的时候,必须指明具体的数据类型(如Box<int>Box<string>),否则是无法实例化的。

2)多类型参数

  • 泛型类可以接受多个类型参数。例如,一个字典类可能需要键和值两种类型:
public class Dictionary<TKey, TValue>
{// 实现细节...
}
  • 根据实际业务需求,可以定义多个泛型 类型参数 ,如 MyGeneric<T1,T2>
    public class MyGenericClass<T1,T2>{public void Test(T1 t1,T2 t2){Console.WriteLine($"result:T={t1.GetType().Name};V={t2.GetType().Name}");}}

3) 泛型类和普通类

泛型类定义的时候与普通使用上基本相同,只不过类名后面多了个尖括号,尖括号中放了泛型参数用于占位

//普通类
public class MyClass
//泛型类
public class MyGenericClass<T>

3. 泛型接口

泛型接口(如IEnumerable<T>)支持统一操作不同数据类型的集合。

public interface IRepository<T>
{T GetById(int id);void Add(T item);void Update(T item);void Delete(T item);
}

在这个例子中,IRepository 是一个泛型接口,T 是类型参数。我们可以为不同的类型实现这个接口:

public class User
{public int Id { get; set; }public string Name { get; set; }
}public class UserRepository : IRepository<User>
{public User GetById(int id){// 实现逻辑return new User { Id = id, Name = "John Doe" };}public void Add(User item){// 实现逻辑}public void Update(User item){// 实现逻辑}public void Delete(User item){// 实现逻辑}
}

4. 泛型方法

泛型方法允许我们在方法级别上使用类型参数。这使得方法可以独立于具体类型,从而提高代码的复用性。

1) 泛型方法

除了类之外,我们还可以定义泛型方法,即使它们所在的类不是泛型类:

public class Utility
{public static void Swap<T>(ref T a, ref T b){T temp = a;a = b;b = temp;}
}

在这个例子中,Swap方法可以交换任意类型的两个变量的值。

public class Utility
{public static T GetMax<T>(T a, T b) where T : IComparable<T>{return a.CompareTo(b) > 0 ? a : b;}
}

在这个例子中,GetMax<T> 是一个泛型方法,T 是类型参数。它接受两个参数并返回较大的值。我们可以通过指定具体的类型来调用这个方法:

int maxInt = Utility.GetMax(10, 20);
Console.WriteLine(maxInt); // 输出:20string maxString = Utility.GetMax("Apple", "Banana");
Console.WriteLine(maxString); // 输出:Banana

2)泛型方法和普通方法

    public class MyGeneric<T>{public MyGeneric(T t){Console.WriteLine($"result:type={t.GetType().Name};value={t}");}public void Show(T t){Console.WriteLine($"result:type={t.GetType().Name};value={t}");}public void Test<V>(V v){}}

在这个例子中,Show 是 具有泛型参数的 普通方法,Test 是泛型方法,MyGeneric<T>是泛型类。

5. 泛型委托

泛型委托允许我们定义通用的委托类型,其行为可以独立于具体的数据类型。

public delegate T MyDelegate<T>(T a, T b);public class Program
{public static T GetMax<T>(T a, T b) where T : IComparable<T>{return a.CompareTo(b) > 0 ? a : b;}static void Main(){MyDelegate<int> intDelegate = GetMax;int maxInt = intDelegate(10, 20);Console.WriteLine(maxInt); // 输出:20MyDelegate<string> stringDelegate = GetMax;string maxString = stringDelegate("Apple", "Banana");Console.WriteLine(maxString); // 输出:Banana}
}

在这个例子中,MyDelegate 是一个泛型委托,它接受两个参数并返回一个值。我们可以通过指定具体的类型来使用这个委托。

6. 静态成员与泛型

若误以为静态成员跨类型共享,可能导致数据不一致或逻辑错误。示例如下:

public class StaticGeneric<T>
{public static int Count;public StaticGeneric(){Count++;}
}
public class Program
{public static void Main(){StaticGeneric<int> staticGenericInt=new StaticGeneric<int>();Console.WriteLine($"Count = {StaticGeneric<int>.Count}");       //输出:Count = 1StaticGeneric<string> staticGenericString=new StaticGeneric<string>();       Console.WriteLine($"Count = {StaticGeneric<string>.Count}");    //输出:Count = 1}
}

在C#中,静态成员与泛型结合使用时需特别注意以下几点

  • 静态成员不共享:
    • 不同类型参数的泛型实例会生成独立的静态成员,导致数据无法共享。
  • 避免在泛型类型中声明静态成员:
    • 泛型类型(如StaticGeneric<T>)的静态成员与具体类型参数绑定,不同类型参数的实例视为不同类型。
    • StaticGeneric<int>.Count 结果为1,StaticGeneric<string>.Count结果仍为1

7. 泛型与default、dynamic关键字

1)与 default

  • 获取类型默认值

    • 在泛型中处理默认值时,可以使用default来获取类型的默认值。
    • 对于引用类型,默认值为null;对于数值类型,默认值为0。
    public T GetDefault<T>()
    {return default(T); // 引用类型返回null,值类型返回0等
    }
    

    从 C# 7.1 开始,可以直接使用 default 而不带括号来简化语法:

    class GenericExample<T>
    {public T GetDefaultValue(){return default;}
    }
    
  • 设置类型默认值

class GenericExample<T,V>
{private T t;private V v;public GenericExample(){t = default;v = default;}
}

2) 与 dynamic

在这里插入图片描述
如上图,Add用于计算t1和t2之和的时候,直接使用int sum= t1+t2,会报错,因为还没有实例化,没有指定数据类型,无法直接适用于加法,但是如果使用dynamic,就可以跳过编译类型检查,改为在运行时解析这些操作。 就可以完成相关的业务逻辑。

8. 协变与逆变中的泛型

  • 协变(Covariance):允许子类泛型赋值给父类(IEnumerable<string>IEnumerable<object>
  • 逆变(Contravariance):允许父类泛型赋值给子类(Action<object>Action<string>

C#支持通过outin关键字来标记泛型参数是否支持协变或逆变。

//协变接口
public interface ICovariant<out T>
{T Get();
}//逆变接口
public interface IContravariant<in T>
{void Set(T item);
}

集合接口的智能转换:

// 支持将派生类集合赋值给基类变量
IEnumerable<string> strings = new List<string>();
IEnumerable<object> objects = strings; // 协变(Covariance)// 允许处理基类的接口处理派生类
Action<object> objectAction = obj => Console.WriteLine(obj);
Action<string> stringAction = objectAction; // 逆变(Contravariance)

关于 协变与逆变 详见:C# 协变与逆变深入解析

三、泛型约束

1. 泛型约束概览

在这里插入图片描述
常用泛型约束概览

约束类型语法说明
基类约束where T : BaseClassT 必须继承自某个基类
接口约束where T : IInterfaceT 必须实现某个接口
值类型约束where T : structT必须是值类型
引用类型约束where T : classT必须是引用类型
无参构造函数where T : new()T必须有无参构造函数

2. 使用泛型约束

  • 有时候,我们可能想要对泛型中的类型参数施加一些限制,比如要求该类型必须实现某个接口或继承自某个基类。这时,我们可以使用约束。
  • 泛型约束通过where关键字限制类型参数,增强安全性

1)单个约束示例


引用类型约束示例:

	public class MyClassGeneric<T> where T : class{public MyClassGeneric(T t){Console.WriteLine($"result:type={t.GetType().Name};value={t}");}}public class User{public int Id { get; set; }public string Name { get; set; }}public class Program{public static void Main(){MyClassGeneric<string> myClassGeneric1 = new MyClassGeneric<string>("test");MyClassGeneric<User> myClassGeneric2 = new MyClassGeneric<User>(new User() { Id=1,Name="Jack"});}}

这里的where T : class表示类型参数T必须是引用类型。如示例中 使用int 类型,则会报错。


值类型约束示例:

    public class MyStructGeneric<T> where T : struct{public My_Generic(T t){Console.WriteLine($"result:type={t.GetType().Name};value={t}");}}//使用MyStructGeneric<int> myStructGeneric = new MyStructGeneric<int>();

这里的where T : struct表示类型参数T必须是不可为null的值类型。如示例中 使用int 类型,如果使用 string 类型则会报错,因为string 是引用类型。


接口/基类约束示例:

public class Box<T> where T : IComparable<T>
{private T _item;public void Set(T item){_item = item;}public T Get(){return _item;}
}

在这个例子中,Box<T> 的类型参数 T 被限制为必须实现 IComparable<T> 接口。这意味着我们只能使用满足该约束的类型来实例化 Box<T>

    public interface IPeople{void GetUserInfo();}public class Chinese : IPeople{public void GetUserInfo(){//throw new NotImplementedException();}}//这个泛型类规定必须是IPeople或者是继承于Ipeople的数据类型才可传入public class MyGeneric4<T> where T : IPeople{public MyGeneric4(){}}//使用MyGeneric4<IPeople> myGeneric4 = new MyGeneric4<IPeople>();MyGeneric4<Chinese> my_Generic44 = new MyGeneric4<Chinese>();

使用的时候 T 必须是IPeople或者是继承于Ipeople的数据类型才可传入


无参数构造函数 约束示例:

    public class MyGeneric3<T> where T : new (){public MyGeneric3(T t){Console.WriteLine($"result:type={t.GetType().Name};value={t}");}}public class User{public int Id { get; set; }public string Name { get; set; }}public class Score{public Score(string code){//有参数的构造函数}}
// 如果这样使用就会报错
// MyGeneric3<Score> my_Generic3 = new MyGeneric3<Score>(new Score()); 
// 这样使用则没有问题
MyGeneric3<User> my_Generic3 = new MyGeneric3<User>(new User()); 

上例中,如果将Score 类 作为类型参数 传入,则会报错,因为该约束限制类型参数必须 有一个无参数的构造函数

2)组合约束示例

public T CreateInstance<T>() where T : Animal, IFly, new()
{return new T();
}
public class GenericClass<T> where T : IComparable, new()
{public void DoSomethingWithGeneric(T input){if (input.CompareTo(default(T)) > 0){Console.WriteLine("Greater than default.");}}
}

注意:

  • 约束可以组合使用
  • 与其他约束一起使用时,new() 约束必须最后指定。

三、为什么使用泛型?

1. 类型安全性

1) 非泛型集合示例

非泛型集合(如ArrayList)存储object类型,需显式转换且易引发运行时错误:

ArrayList list = new ArrayList();
list.Add(1);
list.Add("text");
int num = (int)list[1]; // 运行时异常!

在泛型出现前,ArrayList等集合类以object存储元素,导致:

ArrayList list = new ArrayList();
list.Add(1);    // 装箱
int num = (int)list[0]; // 拆箱 + 类型不安全

2) 泛型集合示例

泛型集合(如List<T>)在编译时即强制类型匹配,杜绝此类问题。
泛型集合List<T>彻底解决了这些问题:

List<int> numbers = new List<int>();
numbers.Add(42); // 无需装箱
numbers.Add("text"); // 编译时直接报错!
int val = numbers[0]; // 直接获取int类型

2. 性能优化

泛型避免装箱(Boxing)与拆箱(Unboxing)操作。例如,List<int>直接操作值类型,而ArrayList需将int装箱为object,显著提升效率。

值类型处理效率对比测试:

操作类型1000万次操作耗时
ArrayList520ms
List<T>85ms
提升幅度6倍+

原因剖析:

  • 避免值类型装箱(Heap内存分配)
  • 消除类型检查开销

性能测试

public class Program
{static void Main(){// 使用非泛型集合ArrayList arrayList = new ArrayList();for (int i = 0; i < 100_0000; i++){arrayList.Add(i);}// 使用泛型集合List<int> list = new List<int>();for (int i = 0; i < 100_0000; i++){list.Add(i);}// 测试性能Stopwatch sw = Stopwatch.StartNew();foreach (var item in arrayList){int value = (int)item; // 装箱和拆箱操作}sw.Stop();Console.WriteLine($"非泛型集合:{sw.ElapsedMilliseconds} ms");sw.Restart();foreach (var item in list){int value = item; // 无需装箱和拆箱}Console.WriteLine($"泛型集合:{sw.ElapsedMilliseconds} ms");}
}

运行结果:

非泛型集合:28 ms
泛型集合:7 ms

在这个例子中,使用泛型集合 List 的性能明显优于非泛型集合 ArrayList,因为泛型集合避免了装箱和拆箱操作。

3. 代码复用性

通过泛型可编写通用逻辑,适应多种数据类型。例如,泛型方法Swap<T>可交换任意类型的变量:

void Swap<T>(ref T a, ref T b) {T temp = a;a = b;b = temp;
}

四、泛型应用场景

1. 泛型与反射

反射(Reflection)允许我们在运行时检查和操作类型的信息。泛型与反射结合使用时,可以实现非常灵活的动态行为。

using System;
using System.Reflection;public class Box<T>
{private T _item;public void Set(T item){_item = item;}public T Get(){return _item;}
}public class Program
{static void Main(){Box<int> intBox = new Box<int>();intBox.Set(42);Type boxType = intBox.GetType();FieldInfo field = boxType.GetField("_item", BindingFlags.NonPublic | BindingFlags.Instance);object value = field.GetValue(intBox);Console.WriteLine(value); // 输出:42}
}

动态创建泛型实例

Type openType = typeof(List<>);
Type closedType = openType.MakeGenericType(typeof(int));
object list = Activator.CreateInstance(closedType);
Type openType = typeof(Dictionary<,>);
Type closedType = openType.MakeGenericType(typeof(int), typeof(string));
object dict = Activator.CreateInstance(closedType);

当编译器无法推断类型时:

// 错误示例
var result = CreateInstance(typeof(List<>)); // 正确写法
var listType = typeof(List<>);
var specificType = listType.MakeGenericType(typeof(int));
var instance = Activator.CreateInstance(specificType);

2. 泛型在依赖注入中的应用

通过泛型接口与DI容器结合,实现服务通用化:

services.AddScoped(typeof(IValidator<>), typeof(ProductValidator));

此配置可为所有实体类型自动提供验证逻辑。

3. 泛型缓存特性

public class Cache<T>
{public static DateTime CreatedTime { get; } = DateTime.Now;// 每个不同的T类型都会创建独立的静态字段
}

4. 泛型与设计模式

仓储模式的现代化实现:

public interface IRepository<T> where T : class 
{T GetById(int id);void Add(T entity);
}public class UserRepository : IRepository<User> 
{// 具体实现
}// 依赖注入配置
services.AddScoped<IRepository<User>, UserRepository>();

泛型工厂模式

public interface IFactory<T>
{T Create();
}public class CarFactory : IFactory<Car>
{public Car Create() => new SportsCar();
}

5. 集合框架

C#的集合框架大量使用了泛型,如List<T>Dictionary<TKey, TValue>等,它们都提供了类型安全的操作,并避免了装箱拆箱带来的性能损耗。

var list = new List<int>();
list.Add(1);
Console.WriteLine(list[0]); // 输出: 1

6. 自定义泛型

创建自己的泛型类或方法可以帮助我们编写更具复用性的代码。比如,创建一个简单的缓存机制:

public class Cache<TKey, TValue>
{private Dictionary<TKey, TValue> _cache = new Dictionary<TKey, TValue>();public void Add(TKey key, TValue value){_cache[key] = value;}public TValue Get(TKey key){return _cache[key];}
}

五、最佳实践

1. 合理使用泛型

  • 泛型可以提高代码的复用性和性能,但并不是万能的。在某些场景下,过度使用泛型可能会导致代码难以理解和维护。因此,我们需要根据实际情况合理使用泛型。
  • 避免过度泛型化,避免过度嵌套泛型类型
  • 类型参数命名,使用TTKeyTValue等有意义的类型参数名

2. 避免过多的类型参数

  • 过多的类型参数会增加代码的复杂性。一般来说,类型参数的数量不应超过 3 个。如果需要更多类型参数,可以考虑将多个类型合并为一个元组或自定义类型。

3. 使用类型约束

  • 类型约束可以限制类型参数的范围,从而提高代码的安全性和灵活性。合理使用类型约束可以避免不必要的运行时错误。

结语

回到目录页:C#/.NET 知识汇总
希望以上内容可以帮助到大家,如文中有不对之处,还请批评指正。


参考资料:
.NET泛型集合源码解析
泛型与设计模式实践
微软官方泛型文档
泛型性能优化白皮书
设计模式中的泛型应用案例集

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com