在C#语言发展早期,开发者处理集合与通用对象容器时主要依赖非泛型类。非泛型类是指在定义时不使用类型参数、其内部成员以object类型接纳任意数据的类。这类类在.NET Framework 1.0时代广泛使用,虽然现在泛型已成主流,但理解非泛型类的运行机制对维护遗留系统、阅读基础类库源码仍有价值。下面通过具体例子说明非泛型类的结构与实际用法。
一、什么是非泛型类
非泛型类与泛型类的最大区别在于:泛型类通过<T>这类类型参数在编译期锁定数据类型,而非泛型类直接把字段、属性或方法参数声明为object。因为object是所有类型的基类,所以非泛型类可以接收任何实例,这种写法带来了极大的灵活度,但也埋下了类型安全的隐患。
以最常见的非泛型集合ArrayList为例,它内部维护一个object数组,添加元素时统一转为object存储。当你取出元素时,必须手动强制转换回原类型,一旦转换类型不匹配就会抛出InvalidCastException。以下代码展示了一个自定义非泛型缓存类的简单声明:
using System;
using System.Collections;
// 自定义非泛型缓存类,可存任意对象
public class SimpleCache
{
private Hashtable _store = new Hashtable();
// 放入缓存,key为字符串,value为任意对象
public void Put(string key, object value)
{
_store[key] = value;
}
// 取出缓存,返回object需调用方自行转换
public object Get(string key)
{
return _store[key];
}
}
上面这段代码没有使用任何类型参数,Put方法接收object类型的value,因此既能存字符串也能存整数。这种写法在不确定数据种类的场景下很方便,但正如后面会提到的,它牺牲了编译期检查能力。
二、非泛型类的实例创建与调用
非泛型类的实例化过程和普通类完全一致,使用new关键字调用其构造函数即可。由于类型没有参数化,构造时不需要指定类型实参。我们基于前面的SimpleCache类,演示如何存入和读取不同类型的数值。
下面代码向缓存分别放入一个字符串与一个整数,读取时分别转换。注意转换时必须使用正确类型,否则运行时会报错。这种使用方式在旧版代码中十分普遍,也是很多初学者接触集合的第一步。
using System;
class Program
{
static void Main()
{
SimpleCache cache = new SimpleCache();
cache.Put("name", "张三");
cache.Put("age", 28);
// 读取并转换
string name = (string)cache.Get("name");
int age = (int)cache.Get("age");
Console.WriteLine("姓名:" + name);
Console.WriteLine("年龄:" + age);
// 若误转换类型,如将age当string取,运行时会异常
// string wrong = (string)cache.Get("age");
}
}
从调用过程可以看出,非泛型类在写法上非常直接,不需要关心类型参数。但这种自由是有代价的:编译器无法帮你确认Get返回的是否真是目标类型,错误只能留到程序运行后才被发现。
三、非泛型类的优点与缺陷
非泛型类最明显的优点是兼容性强。在泛型尚未出现的.NET版本中,它是实现通用容器的唯一手段。即便现在,当你需要快速写一个只处理少量数据、类型混杂的小工具,或者对接某些只暴露object接口的旧API时,非泛型类依然顺手。
但它的缺陷同样突出。首先是类型不安全,如前文代码所示,取出数据时的强制转换可能在运行时失败。其次是性能问题:值类型存入object字段会发生装箱,取出时要拆箱,频繁操作会产生大量临时对象和GC压力。下列表格对比了非泛型与泛型在核心维度的差异:
| 对比维度 | 非泛型类 | 泛型类 |
|---|---|---|
| 类型安全 | 编译期不检查,运行期可能异常 | 编译期确定类型,错误早暴露 |
| 性能表现 | 值类型需装箱拆箱 | 无装箱拆箱开销 |
| 使用灵活性 | 可接收任意类型 | 限定为指定类型或其子类 |
因此在新项目中,官方推荐优先使用List<T>、Dictionary<TKey, TValue>等泛型类型。但在阅读早期代码或封装兼容层时,掌握非泛型类的实例写法能帮你少走弯路。
四、非泛型类在实践中的注意事项
如果你不得不在生产代码中编写非泛型类,建议对公开方法做必要的类型判断,用is或as运算符降低崩溃风险。例如Get方法可改为尝试转换并返回是否成功的模式,让调用方更安全地处理结果。
此外,非泛型类常与反射配合实现通用逻辑,比如根据配置动态创建实例并缓存。此时要注意object到具体类型的转换集中在一处管理,避免散落在各业务方法里难以排查。以下示例展示带类型检查的改进版Get:
using System;
public class SafeCache
{
private Hashtable _store = new Hashtable();
public void Put(string key, object value)
{
_store[key] = value;
}
// 安全获取,转换失败返回null且不抛异常
public T Get<T>(string key) where T : class
{
object val = _store[key];
return val as T;
}
}
这个改进版本虽然引入了泛型方法,但类本身仍是非泛型结构,兼顾了旧有容器与基础类型安全。对于纯非泛型场景,也可在内部用is判断后抛出自描述异常,提升可维护性。
五、总结
非泛型类通过object类型实现了最大程度的通用性,是.NET早期重要的编程构件。它的实例详解核心在于理解其无类型参数、运行时转换、装箱损耗三大特征。在现代化开发中,除非维护老系统或写临时脚本,否则应倾向泛型方案。弄清非泛型类的原理与用法,能让你在遭遇ArrayList、Hashtable等旧类型时从容应对,也更能体会泛型设计带来的编译期保障与性能优化。