目录
相信读者看了上一篇文章之后,大概了解了什么是里氏替换原则,今天我们就来看看在C#中所有类的父类是谁!
一、C#中的万物之父object是什么?
在C#中,object
类型是所有类型的基类(基类即父类)。无论是值类型(如int
、double
、struct
)还是引用类型(如string
、自定义类),最终都直接或间接继承自object
。这意味着,任何变量都可以被隐式或显式地转换为object
类型。
int num = 42;
object obj = num; // int类型隐式转换为object
object
的存在为C#提供了类型统一性的底层支持,使得开发者可以通过基类引用操作任意类型的对象,这在实现多态、泛型等特性时尤为重要。
万物之父
关键字object
概念:
object是所有类型的基类,他是一个类(引用类型)
作用:
1.可以利用里氏替换原则,用object容器装所有对象
2.可以用来表示不确定类型,作为函数参数类型
二、object类提供的基本方法与用途
1. 核心方法
object
类为所有类型提供了以下基础方法:
- ToString():返回对象的字符串表示。默认返回类型的全名(如
System.Int32
),但通常会被子类重写(如int
重写后返回数字本身)。
示例:
int num = 100;
Console.WriteLine(num.ToString()); // 输出 "100"(int重写了ToString)
Person p = new Person { Name = "Alice" };
Console.WriteLine(p.ToString()); // 输出 "Person"(未重写时)
// 重写后:
public class Person {
public string Name { get; set; }
public override string ToString() => $"Name: {Name}";
}
// 输出 "Name: Alice"
- Equals(object obj):判断对象是否相等。默认比较引用(对引用类型),值类型需重写以实现值比较。这里的值重写是指什么呢,我思考的是取掉本身某一些不需要比较的逻辑,添加属于自己特殊的比较方式。例如你有一个圆的类,你想比较每个圆面积的大小,这个时候就可以重写这个函数,实现某些特殊需求。
示例:
int x = 5, y = 5;
Console.WriteLine(x.Equals(y)); // True(值类型比较值)
object obj1 = x, obj2 = y;
Console.WriteLine(obj1.Equals(obj2)); // True(装箱后值仍相等)
Person p1 = new Person { Name = "Bob" };
Person p2 = new Person { Name = "Bob" };
Console.WriteLine(p1.Equals(p2)); // False(引用不同,除非重写Equals)
//比较地址哈
- GetType():返回对象的运行时类型(
Type
对象),用于反射。方法返回对象的运行时类型(即实际类型)后面当我们学习到反射时 我们还会见面的!
示例:
object obj = "Hello";
Type type = obj.GetType();
Console.WriteLine(type.Name); // 输出 "String"
Console.WriteLine(type.IsValueType); // 输出 False
- GetHashCode():生成对象的哈希码,用于哈希表(如
Dictionary
)中的快速查找。
补充知识:
1. 什么是哈希码?
- 哈希码(Hash Code) 是一个由对象内容生成的整数值,用于在哈希表(如
Dictionary
、HashSet
)中快速定位数据。 - 规则:
- 若两个对象相等(
Equals
返回true
),它们的哈希码必须相同。 - 哈希码应尽量均匀分布,减少哈希冲突。
- 若两个对象相等(
2. 默认行为
- 引用类型:默认基于对象地址生成哈希码(即使内容相同,不同实例的哈希码也不同)。
- 值类型:默认基于字段值生成哈希码。
哈希码就相当于每个对象或者值的身份证,不过不同的是可能会有哈希冲突(多个人对应一个身份证)。身份证肯定每人一个,有了哈希码可以干什么呢,当然是查询啦!哈希表可以非常快速的查询某一个元素,就像班级的点名表一样,老师只要挨着挨着点,就会发现那些同学今天翘课了
string s1 = "A";
string s2 = "A";
Console.WriteLine(s1.GetHashCode() == s2.GetHashCode()); // True
你也可以自己重写生成对象哈希码函数,毕竟这是系统写的,他能写,咱也能写。只不过没他写得好罢了,中心思想就是一个尽可能减少哈希冲突。
示例:
public class Person {
public string Name { get; set; }
public int Age { get; set; }
public override int GetHashCode() {
// 组合字段生成哈希码
return Name.GetHashCode() ^ Age.GetHashCode();
}
public override bool Equals(object obj) {
if (obj is Person other) {
return Name == other.Name && Age == other.Age;
}
return false;
}
}
// 使用示例
Person p1 = new Person { Name = "Alice", Age = 30 };
Person p2 = new Person { Name = "Alice", Age = 30 };
Console.WriteLine(p1.GetHashCode() == p2.GetHashCode()); // 输出 True(已重写)
2. 基类的用途
多态(Polymorphism)
- 如何通过
virtual
和override
关键字实现方法重写。 - 接口(
interface
)如何扩展多态行为。 - 学习章节:面向对象编程(OOP)进阶。
- 如何通过
泛型容器(Generic Collections)
- 为什么
List<T>
比ArrayList
更高效且类型安全。 - 泛型约束(
where T : class
)的设计模式。 - 学习章节:集合与泛型编程。
- 为什么
反射(Reflection)
- 如何通过
Type
和Activator
动态创建对象。 - 反射在框架开发(如依赖注入)中的应用。
- 学习章节:高级C#特性与反射机制。
- 如何通过
三、装箱(Boxing)与拆箱(Unboxing)
补充知识:
在C#中,堆和栈是两种不同的内存区域,用于存储程序运行时的数据:
栈(Stack)
- 特点:由系统自动分配和释放,内存大小固定,存取速度快。小
- 存储内容:
- 值类型变量(如
int
、double
、struct
)。- 方法调用的上下文(如局部变量、参数、返回地址)。
- 生命周期:变量在方法执行时创建,方法结束后自动销毁。
堆(Heap)
- 特点:由开发者或垃圾回收器(GC)管理,内存动态分配,存取速度较慢。大
- 存储内容:
- 引用类型对象(如
string
、class
实例)。- 被装箱的值类型(即
object
包裹的值类型)。- 生命周期:对象在
new
时创建,由GC在无引用时回收。
注意哈,这里的堆和栈是和数据结构中的堆和栈不一样哈,那是一种特殊的数据结构,这里是计算机的内存分布
为什么装箱涉及堆?
值类型原本在栈上,但装箱时会将其复制到堆中,以便通过object
引用统一操作。
1. 装箱(Boxing)
定义:将值类型转换为object
引用类型的过程。
过程:
- 在堆(Heap)中分配内存,用于存储值类型的副本。
- 将栈(Stack)中的值类型数据复制到堆中的新对象。
- 返回新对象的引用。
int value = 100;
object boxed = value; // 装箱发生!
发生条件
用object 存值类型(装箱)
再把object 转为值类型(拆箱)装箱
把值类型用引用类型存储
栈内存会迁移到堆内存中
2. 拆箱(Unboxing)
定义:将object
引用类型转换回原始值类型的过程。
过程:
- 检查
object
引用是否为目标值类型的装箱实例。 - 将堆中的数据复制回栈中的值类型变量。
int unboxed = (int)boxed; // 拆箱成功(类型匹配)
// int error = (short)boxed; // 运行时错误:类型不匹配
补充知识点:装箱过程中值类型和引用类型的变化:
1. 值类型装箱后修改
值类型装箱时会创建副本,因此修改原始值不会影响已装箱的副本,反之亦然。
示例:
int original = 10;
object boxed = original; // 装箱,创建副本
original = 20; // 修改原始值
Console.WriteLine(original); // 输出 20
Console.WriteLine((int)boxed); // 输出 10(未受影响)注意喔这里也是拆箱哦
// 尝试修改装箱后的值(需先拆箱)
int unboxed = (int)boxed;
unboxed = 30;
Console.WriteLine(unboxed); // 输出 30
Console.WriteLine((int)boxed); // 输出 10(仍不受影响)
2. 引用类型装箱后修改
引用类型装箱时仅复制引用(即“装箱”在语义上无实际操作),因此修改对象内容会影响所有引用。
public class Data {
public int Value { get; set; }
}
Data original = new Data { Value = 10 };
object boxed = original; // 装箱(仅复制引用)
original.Value = 20; // 修改原对象的属性
Console.WriteLine(original.Value); // 输出 20
Console.WriteLine(((Data)boxed).Value); // 输出 20(指向同一对象)
((Data)boxed).Value = 30; // 修改装箱后的对象
Console.WriteLine(original.Value); // 输出 30(同步变化)
- 值类型:装箱后与原数据完全独立,修改互不影响。
- 引用类型:装箱后与原数据指向同一对象,修改会同步。
四、装箱与拆箱的优缺点
优点
- 灵活性:允许值类型在需要引用类型的场景中使用(如非泛型集合)。
- 兼容性:为遗留代码(如.NET 1.0的非泛型集合)提供支持。
缺点
- 性能损耗:
- 装箱:涉及堆内存分配和数据复制,耗时约为20ns(纳秒)。
- 拆箱:需类型检查,数据从堆复制回栈,耗时约为10ns。
- 类型安全风险:拆箱时若类型不匹配会抛出
InvalidCastException
。
// 示例:大量装箱引发的性能问题
ArrayList array = new ArrayList();
for (int i = 0; i < 100000; i++) {
array.Add(i); // 每次Add都会装箱!
}
五、如何避免装箱与拆箱?
- 使用泛型集合:如
List<T>
替代ArrayList
。 - 优先使用接口:例如用
IEquatable<T>
避免值类型比较时的装箱。 - **
object
类型谨慎转型**:尽量通过泛型或is
/as
操作符确保类型安全。
// 使用泛型集合避免装箱
List<int> genericList = new List<int>();
genericList.Add(42); // 无装箱!
总结
- object是类型系统的基石:所有类型隐式继承
object
,支持多态和类型统一。 - 装箱拆箱需慎用:值类型与
object
互转会带来性能开销,高频场景优先选择泛型。 - 方法重写是关键:合理重写
ToString()
、Equals()
和GetHashCode()
可提升代码可读性与性能。 - 堆栈差异影响行为:值类型装箱后独立存在堆中,引用类型装箱仍共享同一对象。
一起加油吧!