65.9K
CodeProject 正在变化。 阅读更多。
Home

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

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (35投票s)

2015年11月11日

CPOL

10分钟阅读

viewsIcon

53586

downloadIcon

1208

这是我正在进行的系列讲座的第三讲。内容涉及类定义,并提供对 .NET 中类型基本概念的理解。

全部课程集


引言

在本文中,我将重点介绍如何使用 C# 在 .NET 中定义类型。我将回顾引用类型和值类型的区别,讨论 System.Object 和类型转换。有人可能会在这里寻找关于封装、继承和多态的解释。我决定为这些概念单独写一篇文章,作为本系列的下一篇,敬请期待……

值类型和引用类型

CLR 支持两种类型的类型:引用类型值类型。值类型包括基本类型(你可以在我关于基本类型的文章 这里 了解更多),枚举和结构。值类型的项存储在栈中,变量的值存储在变量本身中。当单独声明值类型变量时是如此,当它是引用类的一部分时,它位于堆中。所有值类型都派生自 System.ValueType。值类型不能包含 null 值。关于值类型的一个有趣例子是结构。C# 中的结构与类非常相似

  • 它可以包含变量
  • 可以有属性和方法
  • 可以实现接口

它也与类存在区别

  • 变量在声明时不能被赋值,除非它们是 conststatic
  • 结构不能继承结构
  • 结构在赋值时会被复制(新变量的所有字段都会被复制到源变量)并且赋值后,当前变量和源变量引用的是不同的结构
  • 结构在不使用 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 类型提供的 publicprotected 方法:

  • 公共方法
    • 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 类重写了 EqualsToStringGetHashCode。下面提供了演示此功能的代码。

声明

        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 运算符类似于显式类型转换运算符,但如果对象不匹配类型,它不会生成异常。isas 运算符非常相似,但 as 的速度更快。

下面的代码演示了类型转换以及 isas 运算符

声明

        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"
            }

来源

  1. Jeffrey Richter - CLR via C# 4.5
  2. Andrew Troelsen - Pro C# 5.0 and the .NET 4.5 Framework
  3. http://www.introprogramming.info/english-intro-csharp-book/read-online/chapter-14-defining-classes/
© . All rights reserved.