在 C# 中,对象(Object)和类型(Type)是面向对象编程中的重要概念。
对象是一个实际存在的实体,它具有特定的属性和行为。在 C# 中,所有的数据都是通过对象来表示和操作的。对象可以是事物的具体实例,也可以是抽象的概念或概念模型的实例。
类型是对象的蓝图或模板,用于定义对象的结构和行为。每个对象都属于一个特定的类型,它决定了对象可以具有的属性和可以执行的操作。在 C# 中,类型可以是内置类型(如整数、浮点数等,在 C# 基础篇中有简单的介绍),也可以是自定义的类、结构体、接口或枚举。
C# 是一种强类型语言,这意味着每个对象都有一个确定的类型,并且类型在编译时就已经确定,不能随意更改。通过类型,编译器可以检查代码的类型安全性,并执行适当的操作。
C# 中的对象和类型之间有以下关系:
类(Class)是一种自定义类型,它定义了对象的结构和行为。对象是类的实例,通过类可以创建多个具有相同属性和行为的对象。
结构体(Struct)是一种轻量级的类型,类似于类,但更适合表示简单的值类型数据。结构体可以直接存储在栈上,而不需要分配堆内存。
接口(Interface)是一种抽象类型,它定义了一组方法和属性,但没有提供实现。类可以实现一个或多个接口,从而使其具有接口定义的行为。
枚举(Enum)是一种特殊的值类型,它定义了一组命名的常量。枚举可以用于表示一组相关的命名值,如颜色、状态等。
通过使用对象和类型,我们可以在C#中创建具有不同属性和行为的数据结构,并通过类、结构体、接口和枚举来定义和管理这些数据结构。这为我们提供了一种有效的方式来组织和操作数据,并实现面向对象编程的核心概念。
在C#中,类(Class)和结构(Struct)是两种常用的用户定义类型,用于创建对象和封装数据和功能。它们有一些相似之处,但也有一些重要的区别。下面是对C#中的类和结构的介绍:
类(Class):
类是一种引用类型,用于创建对象。类可以包含字段、属性、方法、构造函数、事件和索引器等成员。
类可以通过实例化创建对象,通过关键字 new
来实现。类的对象可以动态地分配在堆上,并通过引用进行访问和传递。
类支持继承,可以派生出子类,并继承父类的成员。这使得类具有层次结构和多态性的特点。
类是按引用传递的,即当将类的实例传递给其他方法或赋值给其他变量时,实际上是传递了引用,而不是对象的副本。
结构(Struct):
结构是一种值类型,用于封装一组相关的数据。结构适用于较小的对象,其实例存储在栈上,而不是堆上。
结构可以包含字段、属性、方法和构造函数等成员,但不支持继承和析构函数。
结构适用于简单的数据传递和轻量级对象,因为它们在传递和赋值时会复制其值,而不是通过引用进行传递。
结构在使用时具有一些限制,例如不能为结构指定默认的无参构造函数,而且结构变量不能为 null。
类和结构的选择:
通常情况下,使用类来表示复杂的对象,需要使用继承、多态和引用传递的特性。
结构适用于较小、轻量级的数据,需要高性能和值传递的场景,或者与本机代码进行交互。
接口是一种抽象类型,它定义了一组方法、属性、事件或索引器的契约。
接口只定义了成员的签名,没有提供实现细节。实现接口的类或结构体必须提供相应成员的实际实现。
一个类可以实现一个或多个接口,通过实现接口,类可以表达它提供了特定的功能或行为。
接口提供了一种多态性的方式,允许将不同的对象视为同一类型,并通过接口引用来调用相应的成员。
枚举是一种特殊的值类型,用于定义一组命名的常量。
枚举允许开发人员使用易于理解的符号名称来表示一组相关的离散值。
枚举成员是枚举类型的常量值,可以是整数、字符或其他整型数据类型。
枚举提供了一种简洁的方式来处理一组有限的选择,使代码更具可读性和可维护性。
可以使用枚举类型来声明变量,接受和存储枚举成员的值,并对其进行比较和操作。
如果类型包含可以改变的成员,它就是一个可变的类型。使用 readonly 修饰符,编译器会在状态改变时报错。状态只能在构造函数中初始化。
如果对象没有任何可以改变的成员,只有只读成员,它就是一个不可变类型。其内容只能在初始化时设置。
不可变类型的一个例子就是 String 类。这个类没有定义任何允许改变其内容的成员。诸如 ToUpper(把字符串更改为大写)的方法总是返回一个新的字符串,但传递到构造函数的原始字符串保持不变。
在C#中,可以通过以下方式创建不可变类型:
使用只读字段和属性:通过将字段和属性声明为只读,限制对其的修改。
使用构造函数初始化:在对象的构造函数中初始化所有的字段和属性,并且在对象创建后不再进行修改。
避免公开可变的成员:不可变类型应该避免公开可以修改状态的方法或属性。
不可变类型在编程中具有许多优点,如代码简化、线程安全性和可靠性等。它们在函数式编程、并发编程和数据共享等场景中特别有用。
在 C# 中,匿名类型(Anonymous Types)是一种临时的、只读的对象类型,用于在运行时创建一个具有一组属性的对象,而无需事先定义该类型。它们通常用于临时存储或传递一组相关的数据。
匿名类型的定义是使用关键字 new
和一个对象初始化器(Object initializer)来创建一个对象。初始化器是一个包含一组属性名称和对应的值的代码块。每个属性都有一个名称和一个相关的值。属性名称可以是任意有效的标识符。
以下是一个使用匿名类型的简单示例:
var person = new
{
Name = "John",
Age = 30,
City = "New York"
};
Console.WriteLine($"Name: {person.Name}, Age: {person.Age}, City: {person.City}");
在上面的示例中,我们创建了一个匿名类型对象 person
,它具有 Name
、Age
和 City
这三个属性。通过使用 var
关键字,编译器会自动推断出 person
对象的类型。
匿名类型的属性是只读的,一旦对象被创建,就不能更改属性的值。这意味着匿名类型对象在定义之后无法修改。它们主要用于临时存储一组相关的数据,并在需要时进行读取。
匿名类型在很多情况下非常有用,特别是在LINQ查询中,它们可以用作临时存储查询结果的容器。此外,它们还可以简化一些临时数据传递的情况,而无需定义新的具名类型。
需要注意的是,匿名类型是一种编译时概念,编译器会自动为每个匿名类型的定义生成一个对应的类。每个匿名类型的定义都是唯一的,即使属性名称和类型相同,它们仍被视为不同的类型。
所有的 .NET类最终都派生自 System.Object。实际上,如果在定义类时没有指定基类,编译器就会自动假定这个类派生自 Object。
对于结构,这个派生是间接的:结构总是派生自 System.ValueType,Sytem.ValueType 又派生自 System.Object。
其实际意义在于,除了自己定义的方法和属性外,还可以访问为 Object 类定义的许多公共的和受保护的成员方法。这些方法可用于自己定义的所有其它类中。
ToString() 方法:是获取对象的字符串表示的一种便捷方式。当只需要快速获取对象的内容,以进行调试时,就可以使用这个方法。在数据的格式化方面,她几乎没有提供选择:例如,在原则上日期可以表示为许多不同的格式,但 DateTime.ToString() 没有在这方面提供任何选择。如果需要更复杂的字符串表示,例如,考虑用户的格式化首选项或区域性,就应实现 IFormattable 接口。
GetHashCode() 方法:如果对象放在名为映射(也称为散列表或字典)的数据结构中,就可以使用这个方法。处理这些数据结构的类使用该方法确定把对象放在结构的什么地方。如果希望把类用作字典的一个键,就需要重写 GetHashCode() 方法。实现该方法重载的方式有一些相当严格的限制。
Equals() 和 ReferenceEquals() 方法:注意有 3 个用于比较对象相等性的不同方法,这说明 .NET Framework 在比较相等性方面有相当复杂的模式。这 3 个方法和比较运算符“==”在使用方式上有微妙的区别。而且,在重写带一个参数的虚 Equals() 方法时也有一些限制,因为 System.Collections 名称空间中的一些基类要调用该方法,并希望它以待定的方式执行。
Finalize() 方法:它是最接近 C++风格的析构函数,在引用对象作为垃圾被收集以清理资源时调用它。Object 中实现的 Finalize() 方法实际上什么也没有做,因而被垃圾收集器忽略。如果对象拥有对非托管资源的引用,则在该对象被删除时,就需要删除这些引用,此时一般要重写 Finalize()。垃圾收集器不能直接删除这些对非托管资源的引用,因为它只负责托管的资源,于是它只能依赖用户提供的 Finalize()。
GetType() 方法:这个方法返回从 System.Type 派生的类的一个实例,因此可以提供对象成员所属类的更多信息,包括基本类型、方法、属性等。System.Type 还提供了 .NET的反射技术的入口点。
MenberwiseClone() 方法:它只复制对象并返回对副本的一个引用(对于值类型,就是一个装箱的引用)。注意,得到的副本是一个浅表复制,即它复制了类中的所有值类型。如果类包含内嵌的引用就只复制引用,而不复制引用的对象。这个方法是受保护的,所以不能用于复制外部的对象。该方法不是虚方法,所以不能重写它的实现代码。