C# 课程 - 第3课:C# 类型设计。你必须知道的类基础知识






4.94/5 (35投票s)
这是我正在进行的系列讲座的第三讲。内容涉及类定义,并提供对 .NET 中类型基本概念的理解。
全部课程集
- C#Lectures - 第 1 讲:
原始类型 - C# 课程 - 第2课:在 C# 中处理文本:char, string, StringBuilder, SecureString
- C# 课程 - 第3课:C# 类型设计。你必须知道的类基础知识
- C# 课程 - 第4课:面向对象编程基础:C# 示例中的抽象、封装、继承、多态
- C# 课程 - 第5课:C# 示例中的事件、委托、委托链
- C# 课程 - 第6课:C# 中的特性和自定义特性
- C# 讲座 - 第 7 讲:
反射( 通过 C# 示例) - C# 课程 - 第8课:灾难恢复。C# 示例中的异常和错误处理
- C# 讲座 - 第 9 讲:
Lambda 表达式 - C# 课程 - 第10课:LINQ 简介,LINQ to Objects 第一部分
- C# 讲座 - 第 11 讲:LINQ to 0bjects 第二部分:非延迟运算符
引言
在本文中,我将重点介绍如何使用 C# 在 .NET 中定义类型。我将回顾引用类型和值类型的区别,讨论 System.Object
和类型转换。有人可能会在这里寻找关于封装、继承和多态的解释。我决定为这些概念单独写一篇文章,作为本系列的下一篇,敬请期待……
值类型和引用类型
CLR 支持两种类型的类型:引用类型和值类型。值类型包括基本类型(你可以在我关于基本类型的文章 这里 了解更多),枚举和结构。值类型的项存储在栈中,变量的值存储在变量本身中。当单独声明值类型变量时是如此,当它是引用类的一部分时,它位于堆中。所有值类型都派生自 System.ValueType
。值类型不能包含 null
值。关于值类型的一个有趣例子是结构。C# 中的结构与类非常相似
- 它可以包含变量
- 可以有属性和方法
- 可以实现接口
它也与类存在区别
- 变量在声明时不能被赋值,除非它们是 const 或 static
- 结构不能继承结构
- 结构在赋值时会被复制(新变量的所有字段都会被复制到源变量)并且赋值后,当前变量和源变量引用的是不同的结构
- 结构在不使用 new 操作符的情况下无法被实例化
在 .NET 中,可以将值类型转换为引用类型。这通常是为了将值类型传递给某个操作引用类型的函数。如果我们想通过引用来操作值类型,我们就需要一种称为装箱的机制。装箱是将值类型转换为引用类型,当发生这种情况时,会执行以下一系列操作:
- 分配值类型字段所需的内存 + 其他必要的字段
- 将引用类型的值从栈复制到堆
- 返回新创建的引用类型的地址
一旦类型被装箱,就可以执行相反的操作,进行拆箱。
下面的代码演示了本节所述的内容
声明
//value types
internal enum eMyNumbers
{
ONE = 1,
TWO,
THREE
}
internal struct ExampleStructure
{
//public ExampleStructure(); - this line will not compile
private int m_intValue;
private string m_stringValue;
public int IntValue
{
get { return m_intValue; }
set { m_intValue = value; }
}
public string StringValue
{
get { return m_stringValue; }
set { m_stringValue = value; }
}
}
//reference types
internal class ExampleClass
{
private int m_intValue = 0;
private string m_StringValue = "default class value";
public int IntValue
{
get { return m_intValue; }
set { m_intValue = value; }
}
public string StringValue
{
get { return m_StringValue; }
set { m_StringValue = value; }
}
}
用法
Console.WriteLine("-----------------REFERENCE TYPES AND VALUE TYPES-------------------");
eMyNumbers enumSample = eMyNumbers.THREE;
//explicit default constructor is called
ExampleStructure structValue = new ExampleStructure();//stored in stack
structValue.IntValue = 5;//changes in stack
structValue.StringValue = " sample string";//changes in stack
//copying on assignment operator below
//creates new structure in stack and copies there all values
ExampleStructure structValue2 = structValue;
Console.WriteLine(structValue.IntValue + structValue.StringValue); //prints "5 sample string"
Console.WriteLine(structValue2.IntValue + structValue2.StringValue); //prints "5 sample string"
Console.WriteLine(Object.ReferenceEquals(structValue, structValue2));//prints false
ExampleClass classSample = new ExampleClass();//stored in heap
classSample.IntValue = 5;//changes in heap
classSample.StringValue = " sample string";
ExampleClass classSample2 = classSample;//copies only reference
Console.WriteLine(classSample.IntValue + classSample.StringValue); //prints "5 sample string"
Console.WriteLine(classSample2.IntValue + classSample2.StringValue); //prints "5 sample string"
Console.WriteLine(Object.ReferenceEquals(classSample, classSample2));//prints true
object o = (object)structValue;//boxing
structValue2 = (ExampleStructure)o;//unboxing
相比之下,作为类(引用类型)的引用类型存储在堆中,引用类型的变量持有指向对象所在堆内存的引用。本文的其余部分将专门讨论引用类型和类设计。
类型设计
面向对象编程基于程序员使用的标准库(如 .NET)中的类型以及他自己定义的类型。当今的软件开发建立在开发人员创建和使用的类(类型)之上,以解决他们的任务和问题。C# 拥有非常完善的工具来构建自己的类型。在 C# 中,类型可以具有以下成员:
- 常量 - 常量成员与类型本身相关,而不是与对象相关。从逻辑上讲,常量是
static
成员。常量不能通过对象名称访问,而必须通过类型名称访问。常量成员只能是内置类型。编译器在编译时会将常量写入类元数据,并且在运行时初始化的用户定义类型不能是常量。实际上,在用户定义类型可以作为常量成员的情况下,就是当它被赋值为null
时。 - 字段 - 可读或可读/写的某种类型的变量。字段可以是
static
的,在这种情况下,它与类型相关,并且对所有对象都相同;如果不是静态的,那么它对每个对象都是唯一的。Static
对象不能通过对象名称访问,而必须通过类型名称访问;非static
成员可以通过对象访问。 - 构造函数 - 用于对象字段初始化的方法。构造函数可以像其他类型方法一样被重载。主要要求是它必须是
public
的,并且名称与类型名称相同。 - 方法 - 类型实现的功能。这是与类型数据、对象数据或其他任何内容的操作相关联的函数。方法可以是
static
或实例的。静态方法通过类型调用,实例方法与对象相关。 - 重载运算符 - 定义当运算符应用于对象时应执行的操作。
- 转换运算符 - 定义一个对象如何转换为另一个对象的方法。
- 属性 - 这是字段的包装器,提供了一种方便的机制,可以在开发人员模式的控制下设置和获取其值。
- 事件 - 向静态或实例方法发送通知的能力。
- 类型 - 嵌套在当前类型中的类型。
类型可见性有三种值:
public
- 使用该类型所在程序集的任何代码都可以看到该类型internal
- 仅在其自身程序集内可见。如果没有为访问修饰符指定值,则默认类型为internal
private
- 嵌套类型的默认值,但嵌套类型也可以具有所有其他修饰符
除了访问修饰符,还可以为类定义应用 partial 关键字。这意味着类的实现分布在多个文件中。
成员可见性有以下值:
private
- 只能由类型成员和嵌套类型访问protected
- 只能由类型成员、嵌套类型和派生类型访问internal
- 只能由当前程序集的方法访问protected internal
- 可以由嵌套类型、派生类型的方法以及当前程序集的方法访问public
- 可供任何程序集的任何方法访问
除了可见性关键字,还可以为成员变量应用 readonly
关键字。Readonly
变量只能在构造函数中赋值。Readonly
可以应用于实例成员和静态成员。
方法是实现特定功能的类型成员。方法可以是 static
或实例的。方法的一种类型是构造函数。构造函数
- 与类型同名
- 不能被继承,这意味着 virtual、new、override、sealed 和 abstract 关键字不能应用于构造函数
- 构造函数可以是静态的(用于类型)或实例的(用于对象)
- 静态构造函数是无参数的,没有可见性修饰符,并且默认是私有的
- 静态构造函数只能修改静态字段
- 可以定义多个具有不同签名的构造函数
- 对于结构,不能定义无参数的默认构造函数。它必须始终是带参数的。
即使不定义构造函数,CLR 也会始终生成一个默认构造函数,该构造函数与类型同名且不接收任何参数。
下面的代码演示了我本节所述的内容
声明
//internal keyword means that class if visible only inside current assembly
internal class DemonstratingType
{
//Constant member that is equal to all objects
//of the class and is not changeable
public const int const_digit = 5;
//static field that is equal to all objects
//and related to type not to object
private static string m_StaticString;
//read only field
//it can be changed only in constructor
public readonly int ReadOnlyDigit;
//static property that wraps static string
public static string StaticString
{
get { return DemonstratingType.m_StaticString; }
set { DemonstratingType.m_StaticString = value; }
}
//not static filed that is unique for each
//object and related to object not to type
private string m_InstanceString;
//instance property that wraps instance field
public string InstanceString
{
get { return m_InstanceString; }
set { m_InstanceString = value; }
}
protected string m_ProtectedInstanceString;
//+ operator overloading
public static string operator+ (DemonstratingType obj1, DemonstratingType obj2)
{
return obj1.m_InstanceString + obj2.m_InstanceString;
}
//type constructor
static DemonstratingType()
{
m_StaticString = "static string default value";
}
//default constructor
public DemonstratingType()
{
m_ProtectedInstanceString = "default value for protected string";
ReadOnlyDigit = 10;
}
//parametrized overloaded constructor
public DemonstratingType(string InstanceStringInitialValue)
{
m_InstanceString = InstanceStringInitialValue;
m_ProtectedInstanceString = "default value for protected string";
ReadOnlyDigit = 10;
}
//static method that is called on type
public static int SummarizeTwoDigits(int a, int b)
{
return a + b;
}
//instance method that is called on object
public int MyDigitPlustTypeConstant(int digit)
{
return digit + const_digit;
}
public string ShowProtectedString()
{
return m_ProtectedInstanceString;
}
//nested type
private class InternalDataClass
{
private int m_x;
private int m_y;
public InternalDataClass(int x, int y)
{
m_x = x;
m_y = y;
}
}
}
//class DerivedDemonstratedType derives DemonstratingType
//this is called inheritance
internal sealed class DerivedDemonstratingType : DemonstratingType
{
//this function changes protected string that we
//derived from parent type in our sample only
//derived class may change protected string
public void ChangeProtectedString(string newString)
{
m_ProtectedInstanceString = newString;
}
}
用法
Console.WriteLine("-----------------DESIGNING TYPES-------------------");
//default constructor
DemonstratingType object1 = new DemonstratingType();
DemonstratingType object2 = new DemonstratingType();
//static field and static property
Console.WriteLine(DemonstratingType.StaticString);//prints "static string default value"
DemonstratingType.StaticString = "this is the static string";
Console.WriteLine(DemonstratingType.const_digit);//prints 5
Console.WriteLine(DemonstratingType.StaticString);//prints "this is the static string"
//instance field and instance property
object1.InstanceString = "object 1 string";
object2.InstanceString = " object 2 string";
Console.WriteLine(object1.InstanceString);//prints "object 1 string"
Console.WriteLine(object2.InstanceString);//prints " object 2 string"
//operator overloading
Console.WriteLine(object1 + object2);//prints "object 1 string object 2 string"
//parametrized overloaded constructor
DemonstratingType object3 = new DemonstratingType("object 3 string");
Console.WriteLine(object3.InstanceString);//prints "object 3 string"
//static method
Console.WriteLine(DemonstratingType.SummarizeTwoDigits(2 , 3));//prints 5
//instance method
Console.WriteLine(object3.MyDigitPlustTypeConstant(5));//prints 10
//inheritance example + protected string example
DerivedDemonstratingType childType = new DerivedDemonstratingType();
//object1 will reference to same object that childType
object1 = childType;
Console.WriteLine(object1.ShowProtectedString());//prints "default value for protected string"
childType.ChangeProtectedString("new value for protected string");
Console.WriteLine(object1.ShowProtectedString());//prints "new value for protected string"
对象创建流程
要创建任何对象,都必须调用 new
操作符。CLR 要求调用 new
来创建任何对象。(对于内置类型,有一种简化对象创建的方式,无需调用 new
操作符,但这更像是例外而非规则)。当你调用 new 时,会发生以下顺序事件:
- CLR 计算存储所有对象字段、所有父类型字段以及两个额外字段所需内存的大小:类型对象指针 和 同步块索引。
- 上一步计算出的内存被分配并初始化为 0。类型对象指针和同步块索引会被初始化。
- 调用对象的构造函数 + 基类的构造函数。换句话说,将调用所有构造函数直到
System.Objects
。
与 C++ 不同,C# 中的 new
操作符没有配对的 delete
操作符。CLR 会自动清理内存,你无需担心。
当对象创建时,所有成员都将初始化为默认值,对于基本类型是零,对于类型是 null
。下表展示了类型的默认值:
字段类型 | 默认值 |
bool | false |
byte | 0 |
char | '\0' |
字符串 | null |
decimal | 0.0M |
double | 0.0D |
float | 0.0F |
int | 0 |
对象引用 | null |
System.Object - .NET 中一切的父类
你可能知道,.NET 中的所有类型都直接或间接派生自 System.Object
。即使是 .NET 中的基本类型也派生自 ValueType
,而 ValueType
又派生自 System.Object
(你可以在我关于基本类型的文章 这里 了解更多)。在本文中,我们将重点关注非基本类型,即类。基于之前的陈述,声明
class A
{
…..
}
and
class A: System.Object
{
…..
}
是等价的。所有类都派生自 System.Object
的事实确保了每个对象或任何类型都拥有从 System.Object
派生的最少方法集。下面,我将描述 System.Object
实现并为每个 .NET 类型提供的 public
和 protected
方法:
公共
方法ToString
- 默认情况下返回类型的全名。通常,开发人员会重写它以实现更有意义的功能。例如,所有基本类型在此方法中返回其值的字符串表示。Equals
- 如果两个对象具有相同的值,则返回true
。可以重写它,并实现自己的对象比较方式。GetType
- 返回Type
类型的对象,该对象标识调用GetType
的对象。Type
类型的对象可用于获取有关调用GetType
的对象的元数据信息。这是使用System.Reflection
命名空间中的类实现的。反射是一个独立的主题,我们在此不深入探讨。GetType
不是虚拟方法,不能重写。基于此,你可以确信GetType
始终返回描述当前对象的有效数据。GetHashCode
- 返回当前对象的哈希码。如果需要,可以重写。
如果你重写了 Equals
,则建议也重写 GetHashCode
。一些处理对象的 .NET 算法要求两个相等的对象应具有相同的哈希码。
受保护
方法MemberwiseClone
- 此方法创建一个新类型项,复制调用它的对象的所有字段。返回新项的引用。Finalize
- 当GarbageCollector
确定对象是垃圾但在此之前释放对象所占用的内存时调用。
为了演示每个类从 System.Object
继承的基本功能,我实现了一个包含 2 个类的简短示例。ObjectExample
类是一个空类,默认继承所有 System.Object
功能,而 ObjectOverrideExample
类重写了 Equals
、ToString
和 GetHashCode
。下面提供了演示此功能的代码。
声明
internal class ObjectExample
{
}
internal class ObjectOverrideExample
{
//static variable is used in our example
//as global counter for all objects of
//type ObjectOverrideExample
private static int ObjectsCounter = 0;
private string m_InternalString;
private int m_OrderNumber;
public int OrderNumber
{
get { return m_OrderNumber; }
set { m_OrderNumber = value; }
}
public string InternalString
{
get { return m_InternalString; }
set { m_InternalString = value; }
}
public ObjectOverrideExample()
{
m_InternalString = " Private string";
ObjectsCounter++;
m_OrderNumber = ObjectsCounter;
}
public override string ToString()
{
//here in addition to the full name of the type
//that System.Object returns we add the value
//of string member
return base.ToString() + m_InternalString;
}
public override int GetHashCode()
{
//instead of HashCode that is implemented
//in System.Object we return their order
//number that we give to each object while
//its creation
return m_OrderNumber;
}
public override bool Equals(object obj)
{
if (obj == null) return false;
//check if input object is of type ObjectOverrideExample
//if so then we compare not that both references point
//to same object but we compare order numbers
if (obj.GetType().FullName == "_03_ClassesStructuresEtc.Program+ObjectOverrideExample")
{
ObjectOverrideExample temp = (ObjectOverrideExample)obj;
if (m_OrderNumber != temp.OrderNumber)
{
return false;
}
return true;
}
//if input object is of different type we compare
//that both objects reference the same memory i.e. use
//parent algorithm
return base.Equals(obj);
}
}
用法
Console.WriteLine("-----------------System.Object-------------------");
ObjectOverrideExample OverrideSample = new ObjectOverrideExample();
Console.WriteLine(OverrideSample.ToString());//prints "_03_ClassesStructuresEtc.Program+
//ObjectOverrideExample Private string"
ObjectExample Sample = new ObjectExample();
Console.WriteLine(Sample.ToString());//prints "_03_ClassesStructuresEtc.Program+ObjectExample"
//GetHashCode
Console.WriteLine(Sample.GetHashCode());
Console.WriteLine(OverrideSample.GetHashCode());//prints 1
for (int i = 0; i < 10; i++) //prints numbers from 2 to 11
{
ObjectOverrideExample tmp = new ObjectOverrideExample();
Console.WriteLine(tmp.GetHashCode());
}
//Equals
ObjectOverrideExample tmp2 = new ObjectOverrideExample();
Console.WriteLine(OverrideSample.Equals(tmp2));//prints true
ObjectOverrideExample tmp3 = OverrideSample;
Console.WriteLine(OverrideSample.Equals(tmp3));//prints true
//GetType
Type OverrideType = OverrideSample.GetType();
Console.WriteLine(OverrideType.FullName);//prints "_03_ClassesStructuresEtc.Program+
// ObjectOverrideExample"
Console.WriteLine(OverrideType.Name); //prints "ObjectOverrideExample"
Type SampleType = Sample.GetType();
Console.WriteLine(SampleType.FullName);//prints
// "_03_ClassesStructuresEtc.Program+ObjectExample"
Console.WriteLine(SampleType.Name); //prints "ObjectExample"
类型转换
在运行时,CLR 始终知道当前对象的类型。如我们所讨论的,每个对象都有 GetType
函数来返回其类型。在任何给定时刻了解每个对象的类型都能保证应用程序正常运行,例如,将正确的对象传递给函数并对其调用正确的方法。但是,为了获得例如对不同对象的相同处理方式,我们经常需要将某些类型转换为其他类型。CLR 支持平滑地转换为父类型。你无需编写任何特定代码即可实现这一点。这称为隐式转换。隐式转换是指从派生类型转换为基类型。如果进行隐式转换后需要转换回来,则需要使用显式转换。显式转换是指从父类转换为子类。
除了隐式和显式转换,C# 还有两个对类型转换有用的运算符。is
运算符检查输入对象是否与当前类型兼容,如果是则返回 true。is
运算符从不生成异常。除了 is
运算符,C# 还有另一个称为 as
的运算符。使用 as
运算符,你可以检查对象是否与类型兼容,如果兼容,as
将返回该对象的非空指针;否则,它将返回 null。as
运算符类似于显式类型转换运算符,但如果对象不匹配类型,它不会生成异常。is
和 as
运算符非常相似,但 as
的速度更快。
下面的代码演示了类型转换以及 is
、as
运算符
声明
internal class ParentClass
{
public virtual void OutputFunction(string s = "")
{
Console.WriteLine("OutputFunction in ParentClass" + s);
}
}
internal class ChildClass_Level1 : ParentClass
{
//ChildClass_Level1 overrides base class function
//to have its own implementation
public override void OutputFunction(string s = "")
{
Console.WriteLine("OutputFunction in ChildClass_Level1" + s);
}
}
internal class ChildClass_Level2 : ChildClass_Level1
{
//ChildClass_Level2 overrides base class function
//to have its own implementation
public override void OutputFunction(string s = "")
{
Console.WriteLine("OutputFunction in ChildClass_Level2" + s);
}
}
用法
Console.WriteLine("-----------------TYPE CASTING-------------------");
//implicit conversion from child to parent, no special syntax is needed
ParentClass parent = new ParentClass();
parent.OutputFunction();//prints "OutputFunction in ParentClass"
ParentClass parent_child1 = new ChildClass_Level1();
parent_child1.OutputFunction();//prints "OutputFunction in ChildClass_Level1"
ParentClass parent_child2 = new ChildClass_Level2();
parent_child2.OutputFunction();//prints "OutputFunction in ChildClass_Level2"
//explicit conversion to cast back to derived type
ChildClass_Level2 child2 = (ChildClass_Level2)parent_child2;
child2.OutputFunction();//prints "OutputFunction in ChildClass_Level2"
ChildClass_Level1 child1 = (ChildClass_Level1)parent_child1;
child1.OutputFunction();//prints "OutputFunction in ChildClass_Level1"
//the code below compiles, but fails in runtime
try
{
child2 = (ChildClass_Level2)parent;
child2.OutputFunction();
}
catch (InvalidCastException e)
{
Console.WriteLine("Catch invalid cast exception : " + e.Message);
}
//to avoid exception above we can use following code and is operator
if (parent is ChildClass_Level2)
{
parent.OutputFunction();//we never reach here
}
if(parent_child2 is ChildClass_Level2)
{
parent_child2.OutputFunction
(" using is");//prints "OutputFunction in ChildClass_Level2 using is"
}
//is operator can be replaced by as operator
child2 = parent as ChildClass_Level2;
if (child2 != null)
{
child2.OutputFunction();//we never reach here
}
child2 = parent_child2 as ChildClass_Level2;
if (child2 != null)
{
child2.OutputFunction
(" using as");//prints "OutputFunction in ChildClass_Level2 using as"
}
来源
- Jeffrey Richter - CLR via C# 4.5
- Andrew Troelsen - Pro C# 5.0 and the .NET 4.5 Framework
- http://www.introprogramming.info/english-intro-csharp-book/read-online/chapter-14-defining-classes/