请解释泛型在 C# 中的⽤法和优势
泛型(Generics)是 C# 中的一种强类型编程特性,允许在定义类、接口、方法和委托时指定类型参数,而无需预先指定具体的数据类型。通过泛型,可以实现类型安全、代码复用以及性能优化。
使用泛型类,可以为不同的数据类型提供通用的实现。
使用泛型方法,可以在方法级别定义类型参数。
泛型接口用于定义带有类型参数的接口。
泛型委托允许委托可以处理不同类型的数据。
泛型在编译时进行类型检查,避免了运行时的类型转换异常。
通过泛型,可以编写一次代码,然后为多个类型使用相同的实现。
泛型避免了使用 object 类型时的装箱(Boxing)和拆箱(Unboxing)操作,从而提高性能。
泛型代码更加直观,便于理解和维护,因为类型信息在编译时就明确了。
C# 提供了一些关键字来对泛型类型参数进行约束:
- 数据结构:如 List<T>、Dictionary<TKey, TValue>。
- 算法库:如快速排序、合并排序的通用实现。
- 依赖注入:泛型接口用于设计灵活的依赖注入系统。
- 类型安全的工具:如泛型的 Swap 方法、类型转换工具等。
C# 中的泛型通过提供类型参数化的能力,不仅提高了代码的复用性,还增强了类型安全性,避免了运行时错误和性能损失。合理利用泛型是设计高效、灵活、可扩展系统的关键。
深入探索 Java 复杂泛型:使用与限制全解析
Java 中的泛型允许在定义类、接口和方法时使用类型参数,提高代码的类型安全性和可重用性。其中复杂泛型更是在实际开发中有着广泛的应用,但也存在一些使用限制。
复杂泛型概述:
泛型,即“参数化类型”。在 Java 中,泛型的本质是为了在不创建新的类型的情况下,通过泛型指定的不同类型来控制形参具体限制的类型。泛型类型用于类的定义中,被称为泛型类;用于方法上,被称为泛型方法;用于接口中,被称为泛型接口。最典型的应用就是各种容器类,如:List、Set、Map。
泛型的引入提高了代码的安全性。在没有泛型的情况下,通过对类型 Object 的引用来实现参数的“任意化”,但这种“任意化”带来的缺点是要做显式的强制类型转换,而这种转换是要求开发者对实际参数类型可以预知的情况下进行的。对于强制类型转换错误的情况,编译器可能不提示错误,在运行的时候才出现异常,这是本身就是一个安全隐患。而泛型提供了编译时类型安全检测机制,所有的强制转换都是自动和隐式的,在编译的时候能够检查类型安全。
例如,在集合类中,不使用泛型时,ArrayList 可以存放任意类型,可能会出现类型转换错误。如下代码:
程序运行结果会以崩溃结束,因为在使用时都以 String 的方式使用,但集合中包含了 Integer 类型,导致类型转换异常。
为了解决类似这样的问题,泛型应运而生。使用泛型后,代码如下:
通过泛型来提前检测类型,编译时就通不过可能出现类型转换错误的代码。
Java 集合框架广泛使用泛型来提供类型安全的集合操作。例如 List<Integer> 表示存储整数的列表,Map<String, Integer> 表示键为字符串、值为整数的映射。在集合类中使用泛型可以确保只存储特定类型的元素,避免了类型转换错误,提高了代码的安全性和可读性。
可以定义自己的泛型类和接口,以实现更灵活的数据结构和算法。例如 class MyGenericClass<T> {… },class MyInterface<T> {… }。泛型类和接口可以存储任何类型的对象,根据具体需求指定类型参数,提高了代码的可重用性和灵活性。
可以在方法中使用泛型,以实现更通用的方法。如 public <T> void printArray(T[] array) {… },可以打印任何类型的数组。泛型方法可以根据传入的参数类型自动推断泛型类型,提高了代码的通用性和可读性。
- 泛型边界:可以使用泛型边界来限制类型参数的类型范围,如 class NumberBox<T extends Number>{…},确保只能存储数字类型的对象。通过泛型边界,可以限制泛型类型的范围,提高代码的安全性和可读性。
- 泛型通配符:泛型通配符可以用于表示未知类型或多种类型,如 class WildcardExample{public static void printList(List<?> list){…}},可以接受任何类型的列表。泛型通配符可以增加代码的灵活性,在处理未知类型的集合时非常有用。
- 泛型方法的多态性:泛型方法可以在不同的类型上表现出多态性,如 class PolymorphicGenericMethod{public static<T>T getMiddleElement(T[] array){…}},可以根据传入的数组类型返回不同类型的中间元素。泛型方法的多态性可以提高代码的通用性和可重用性。
- 泛型接口的实现:可以实现泛型接口,并根据具体需求指定类型参数,如 interface Comparable<T>{int compareTo(T o);},class IntegerComparator implements Comparable<Integer>{…},用于比较整数类型的对象。泛型接口的实现可以提高代码的可扩展性和可维护性。
类型擦除(Type Erasure)是 Java 泛型实现中的一种技术,它在编译时保留泛型类型信息,在生成的字节码中删除泛型类型信息。具体来说,将泛型类型转换为相应的原始类型,例如对于一个泛型类,在编译时会将泛型参数替换成相应的原始类型。如果泛型参数是的,则会被替换成 Object 类型;如果泛型参数是有界的,则会被替换成其边界类型。
在运行时,由于没有泛型类型信息,编译器将对类型进行擦除,导致泛型类型的参数化类型被擦除,而只能使用原始类型来代替。这也就意味着,泛型类型在运行时丢失了一部分类型信息,从而导致了一些限制和局限。
例如,在使用泛型类时,无法获得泛型类型的实际类型参数,也无法使用类型参数的特定方法或属性。这些限制需要在代码中进行额外的判断和处理。
此外,类型擦除还会带来一些其他的影响。比如,不能用基本类型去实例化泛型类型,不能创建泛型的实例,不能声明类型为泛型的静态字段,类型转换或 instanceof 不能和泛型类型一起使用,不能创建参数化类型的数组,不能创建、捕获或抛出泛型类型的对象,不能重载泛型类型擦除后具备相同原始类型的方法等。
为了应对类型擦除带来的影响,可以采取一些策略。比如,使用显式的类型转换和检查,避免在泛型中使用基本数据类型,利用泛型通配符和边界,使用 TypeToken 或类似技巧处理反射和泛型,了解并遵循 Java 泛型的最佳实践等。
- Java 泛型不能使用基本类型,因为泛型在编译时,会进行类型擦除,最后只保留原始类型,而原始类型只能是 Object 类及其子类,当然不能使用基本数据类型。例如,使用基本类型的泛型会编译报错,代码如下:List<int> list = new List<int>();。
- 不允许进行实例化,通过类型擦除,泛型方法会转换为原始方法,可能会导致类型转换错误。比如错误代码如下:<T>void test(T t){t = new T();},通过类型擦除,上面的泛型方法会转换为如下的原始方法:void test(Object t){t = new Object();},而实际使用中,实参 t 一般情况下都是 Object 的子类,此时相当于:Integer t = 1;t = new Object();,而 Object 肯定无法转换成 Integer。Java 泛型为了防止此类类型转换错误的发生,禁止进行泛型实例化。
- 不允许进行静态化,静态变量在类享,而泛型类型是不确定的,因此编译器无法确定要使用的类型。例如参考下面的代码:static class Demo<T>{private static T t;public static T getT(){return t;}},这里的泛型变量 t 会在编译时报错。备注:这里的静态化针对的对象是泛型变量,并不是泛型方法。
- 不允许直接进行类型转换,但通过通配符可以实现类型转换,为了降低类型转换的安全隐患。示例代码如下:List<Integer> integerList = new ArrayList<Integer>();List<Double> doubleList = new ArrayList<Double>();,不能直接进行类型转换,类型检查会报错,即integerList = doubleList;是错误的。因为如果不限制类型转换,很容易产生ClassCastException异常。Java 泛型不允许直接进行类型转换,但是通过通配符可以实现类型转换(不建议这么做)。代码如下:static void cast(List<?> orgin, List<?> dest){dest = orgin;},public static void main(String[] args){List<Integer> integerList = new ArrayList<Integer>();List<Double> doubleList = new ArrayList<Double>();//通过通配符进行类型转换cast(doubleList,integerList);}。
- 不允许直接使用instanceof运算符进行运行时类型检查,但通过通配符可以实现,为了避免类型转换错误。直接使用instanceof运算符进行运行时类型检查:List<String> stringList = new ArrayList<String>();,不能直接使用instanceof,类型检查报错,即LOGGER.info(stringList instanceof ArrayList<Double>);是错误的。我们可以通过通配符的方式进行instanceof运行期检查(不建议):LOGGER.info(stringList instanceof ArrayList<?>);。
- 不允许创建确切类型的泛型数组,但通过通配符可以实现,为了避免类型转换错误。创建确切类型的泛型数组如下:Demo6<Integer>[] iDemo6s = new Demo6<Integer>[2];会类型检查报错。我们可以通过通配符的方式创建泛型数组(不建议):Demo6<?>[] iDemo6s = new Demo6<?>[2];iDemo6s[0]=new Demo6<Integer>(1);iDemo6s[1]=new Demo6<String>(\”hello\”);。
- 不允许定义泛型异常类或者 catch 异常,但可以以异常类作为边界和 throws 泛型类型,为了避免编译错误。直接看代码:// static class Apple<T> extends Exception {// }// static class Orange<T> extends Throwable {// },//7.Java 泛型:可以 throws 泛型类型<T extends Exception>void test7(T t,Class<T> tClass)throws T {try{//…// } catch (T t) {//不允许捕获一个泛型类型的异常}catch(Exception e){//不允许捕获一个泛型类型的异常//…}}。如果假设可以定义泛型异常类,进行类型擦除时会去掉泛型信息,形成同时 catch 两个一样的异常,肯定不能通过编译。Java 泛型为了避免这种错误的产生,不允许定义泛型异常类或者 catch 异常。
- 不允许作为参数进行重载,因为类型擦除之后,两个方法是一样的参数列表,无法重载。例如代码如下:class Test{public void method(List<Integer> list){}public void method(List<String> list){},这样的重载是不被允许的,因为类型擦除后,两个方法的参数列表都是List,无法区分。
本文作者及来源:Renderbus瑞云渲染农场https://www.renderbus.com
文章为作者独立观点不代本网立场,未经允许不得转载。