C# 运算符和类型强制转换

1. 运算符

1.1 运算符的简化操作

为什么用两个例子来分别说明 ++ 递增和 -- 递减运算符?把运算符放在表达式的前面称为前置,把运算符放在表达式的后面称为后置。要点是注意它们的行为方式有所不同。

递增或递减运算符可以作用于整个表达式,也可以作用于表达式的内部。当 i++ 和 ++i 单独占一行时,它们的作用是相同的,对应于语句 i = i + 1。但当它们用于较长的表达式内部时,把运算符放在前面会在计算表达式之前递增 i;换言之,递增了 i 后,在表达式中使用新值进行计算。而把运算符放在后面会在计算表达式之后递增 i ——使用 i 的原始值计算表达式。

例子:

  • i++(后增量运算符):

    • 先使用 i 的当前值,然后将 i 增加 1。

    • 返回 i 的当前值。

    • 例如,如果 i 的初始值为 5,执行 i++ 后,i 的值将变为 6,但整个表达式的结果仍为 5。

  • ++i(前增量运算符):

    • 先将 i 增加 1,然后使用增加后的值。

    • 返回 i 增加后的值。

    • 例如,如果 i 的初始值为 5,执行 ++i 后,i 的值将变为 6,整个表达式的结果也为 6。

这两个运算符的区别在于返回值。如果你使用这些运算符的结果赋值给其他变量或在表达式中使用,就会注意到区别。

例如,考虑以下示例代码片段

int i = 5;
int a = i++;   // a = 5, i = 6
int b = ++i;   // b = 7, i = 7

在第一行中,使用 i++,将 i 的值赋给 a,并且 i 的值递增为 6。因此,a 的值为 5,i 的值为 6。

在第二行中,使用 ++i,先将 i 的值递增为 7,然后将增加后的值赋给 b。因此,b 的值为 7,i 的值也为 7。

总结起来,i++ 是在使用当前值后递增,而 ++i 是先递增后使用增加后的值。这种差异可能会在某些特定的编程场景中产生影响,因此需要根据具体情况选择适当的递增运算符。

在C#中,i++ 和 ++i 的底层原理涉及到编译器生成的 IL(Intermediate Language,中间语言)代码以及对应的机器码。

  • i++(后增量运算符)的底层原理:

    • 编译器会生成 IL 代码,先将 i 的值加载到堆栈顶部。

    • 然后,在堆栈中将 i 的值复制到临时变量中,用作后续表达式的返回值。

    • 接下来,将堆栈中的 i 的值增加 1。

    • 最后,将增加后的 i 的值存回原始的变量 i。

    • 这个过程涉及到堆栈的操作,以及对变量 i 的读取、递增和写回操作。

  • ++i(前增量运算符)的底层原理:

    • 编译器会生成 IL 代码,将 i 的值增加 1。

    • 然后,在堆栈中将增加后的 i 的值复制到临时变量中,用作后续表达式的返回值。

    • 最后,将临时变量中的值存回原始的变量 i。

    • 这个过程也涉及到对变量 i 的递增和写回操作,但没有额外的读取操作。

下面介绍在 C# 代码中频繁使用的基本运算符和类型强制转换运算符。

1.1.1 ?: 条件运算符

条件运算符(?:)也成为三元运算符,是 if...else 结构的简化形式。其名称的出处是它带有 3 个操作数。它首先判断一个条件,如果条件为真,就返回一个值;如果条件为假,则返回另一个值。其语法如下:

​condition ? true_value : false_value​​

其中 condition 是要判断的布尔表达式,true_value 是 condition 为真时返回的值,false_value 是 condition 为假时返回的值。

恰当地使用三元运算符,可以使程序非常简洁。它特别适合于给调用的函数提供两个参数中的一个。使用它可以把布尔值快速转换为字符串值 true 或 false。它也很适合于显示正确的单数形式或复数形式:

int x = 1;
string s = x + " ";
s += x == 1 ? "man" : "men";
Console.WriteLine(s);

1.1.2 ref 条件表达式

在C#中,ref 关键字可以与条件表达式一起使用,以实现对引用类型变量的条件赋值。这种方式允许在条件成立时修改原始变量的值。以下是 ref 条件表达式的语法示例:

condition ? ref trueExpression : ref falseExpression;

其中,condition 是一个布尔表达式,用于确定选择哪个表达式进行赋值。trueExpressionfalseExpression 都是引用类型变量,它们必须具有相同的类型。

下面是一个具体的示例,演示了如何使用 ref 条件表达式来根据条件修改引用类型变量的值:

class Program
{
    static void Main()
    {
        string str1 = "Hello";
        string str2 = "World";

        bool condition = true;

        ref string selectedString = ref (condition ? ref str1 : ref str2);

        selectedString = "Modified";

        Console.WriteLine("str1: " + str1);
        Console.WriteLine("str2: " + str2);
    }
}

在上述示例中,我们有两个字符串变量 str1str2,以及一个布尔变量 condition。我们使用 ref 条件表达式将 selectedString 的引用指向 str1str2,具体取决于 condition 的值。

在代码的最后,我们通过 selectedString 修改了选定的字符串的值。由于 selectedString 是引用类型的引用,实际上是修改了 str1 的值。

输出结果将显示 str1: Modified,而 str2 的值保持不变。

总结起来,使用 ref 条件表达式,可以根据条件选择要修改的引用类型变量,并在条件成立时通过引用来修改原始变量的值。

1.1.3 checked 和 unchecked 运算符

C# 提供了 checked 和 unchecked 运算符。如果把一个代码块标记为 checked,CLR 就会执行溢出检查,如果发生溢出,就抛出 OverflowException 异常。

byte b = byte.MaxValue;
checked
{
    b++;
}
Console.WriteLine(b); // OverflowException: Arithmetic operation resulted in an overflow.

如果要禁止溢出检查,则可以把代码标记为 unchecked:

byte b = byte.MaxValue;
unchecked
{
    b++;
}
Console.WriteLine(b); // 0

在本例中不会抛出异常,但会丢失数据——因为 byte 数据类型不能包含256,溢出的位会被丢弃,所以 b 变量得到的值是0。

注意,unchecked 是默认行为。只有在需要把几行未检查的代码放在一个显示标记为 checked 的大代码块中时,才需要显示地使用 unchecked 关键字。

注意:默认不检查上溢出和下溢出,因为执行检查会影响性能,使用 checked 作为默认设置时,每一个算术运算的结果都需要检验其值是否越界。算术运算也可以用于使用 i++ 的 for 循环中。为了避免这种性能影响,最好一直不使用默认设置,在需要时使用 checked 运算符。

1.1.4 is 运算符

在C#中,is 运算符用于检查一个对象是否与指定类型兼容。它的语法形式如下:

expression is type

其中,expression 是要进行类型检查的表达式,而 type 是要检查的目标类型。is 运算符返回一个布尔值,表示 expression 是否可以转换为 type 类型或其派生类型。

下面是一个示例,演示了如何使用 is 运算符进行类型检查:

class Program
{
    static void Main()
    {
        object obj1 = "Hello";
        object obj2 = 123;
        object obj3 = new Program();

        Console.WriteLine(obj1 is string);   // true
        Console.WriteLine(obj2 is int);      // true
        Console.WriteLine(obj3 is Program);  // true

        Console.WriteLine(obj1 is int);      // false
        Console.WriteLine(obj2 is string);   // false
        Console.WriteLine(obj3 is int);      // false
    }
}

在上述示例中,我们使用 is 运算符检查了不同对象的类型。obj1 是一个字符串,所以 obj1 is string 返回 true。类似地,obj2 是一个整数,所以 obj2 is int 返回 trueobj3 是一个 Program 类型的对象,所以 obj3 is Program 返回 true

另一方面,由于 obj1 不是一个整数,所以 obj1 is int 返回 false。同样地,obj2 不是一个字符串,所以 obj2 is string 返回 falseobj3 也不是一个整数,所以 obj3 is int 返回 false

在C# 9.0及以上版本中,is 运算符可以与匹配模式(Pattern Matching)一起使用,以提供更强大的类型检查和模式匹配功能。使用 is 运算符与匹配模式可以执行以下操作:

  • 类型模式匹配:

if (expression is Type variable) { // 执行与 variable 类型匹配的操作 }

// 在此模式中,expression 是要进行类型检查的表达式,Type 是目标类型,并且 variable 是一个新的变量,用于在匹配成功时将 expression 转换后的值赋给它。如果 expression 可以转换为 Type 类型,条件为真,就会执行相应的操作。



- 常量模式匹配:

  ```csharp
if (expression is constant)
{
    // 执行与 constant 值匹配的操作
}

// 在此模式中,expression 是要进行匹配的表达式,constant 是一个常量值。如果 expression 的值与 constant 相等,条件为真,就会执行相应的操作。

下面是一个示例,演示了 is 运算符与匹配模式的使用

class Program
{
    static void Main()
    {
        object obj = "Hello";

        if (obj is string str)
        {
            Console.WriteLine("字符串长度:" + str.Length);
        }
        else if (obj is int number)
        {
            Console.WriteLine("整数值:" + number);
        }
        else
        {
            Console.WriteLine("未知类型");
        }
    }
}

在上述示例中,我们使用 is 运算符与匹配模式对 obj 进行类型检查。如果 obj 是一个字符串,就将其转换为 string 类型,并执行相应的字符串操作;如果 obj 是一个整数,就将其转换为 int 类型,并执行相应的整数操作。如果 obj 不是字符串也不是整数,则输出 "未知类型"。

1.1.5 as 运算符

as 运算符用于执行引用类型的显式类型转换。如果要转换的类型与指定的类型兼容,转换就会成功进行;如果类型不兼容,as 运算符就会返回 null 值。

object o1 = "Some String";
object o2 = 5;
string s1 = o1 as string; // Some String
string s2 = o2 as string; // null

as 运算符允许在一步中进行安全的类型转换,不需要先进行 is 运算符测试类型,再执行转换。

注意:is 和 as 运算符也用于继承。

1.1.6 sizeof 运算符

使用 sizeof 运算符可以确定栈中值类型需要的长度(单位是字节):Console.WriteLine(sizeof(int)); // 4​​

如果结构体只包含值类型,也可以使用 sizeof 运算符和结构——如下所示的结构:

public struct Point
{
    public int X { get; }
    public int Y { get; }
    public Point(int x, int y)
    {
        X = x;
        Y = y;
    }
}

注意:类不能使用 sizeof 运算符。

如果对复杂类型(而非基本类型)使用 sizeof 运算符,就需要把代码放在 unsafe 块中。

unsafe
{
    Console.WriteLine(sizeof(Point))
}

1.1.7 typeof 运算符

在C#中,typeof 运算符用于获取类型的 System.Type 对象。它的语法形式如下:

typeof(type)

其中,type 是要获取类型的关键字或表达式。typeof 运算符返回一个表示指定类型的 System.Type 对象。

下面是一些示例,演示了如何使用 typeof 运算符获取类型的 System.Type 对象:

Type type1 = typeof(string);
Console.WriteLine(type1);  // 输出 "System.String"

Type type2 = typeof(int);
Console.WriteLine(type2);  // 输出 "System.Int32"

Type type3 = typeof(Program);
Console.WriteLine(type3);  // 输出 "Program"

在上述示例中,我们使用 typeof 运算符获取了不同类型的 System.Type 对象。typeof(string) 返回表示字符串类型的 System.Type 对象;typeof(int) 返回表示整数类型的 System.Type 对象;typeof(Program) 返回表示当前程序类的 System.Type 对象。

通过获取类型的 System.Type 对象,我们可以进行各种类型相关的操作,例如获取类型的名称、基类型、成员信息等。

请注意,typeof 运算符在编译时就确定了类型,并且可以用于获取任何已知的编译时类型的 System.Type 对象。它适用于静态类型和内置类型,以及用户自定义的类、结构体、接口和枚举类型。

1.1.8 nameof 运算符

nameof 表达式(C# 参考)

1.1.9 index 运算符

在C# 8.0及以上版本中,引入了 index 运算符,用于表示访问集合(如数组、列表、字符串等)中的元素,以反向索引的方式。index 运算符使用方括号 [ ],并通过指定索引值来访问元素。

index 运算符有两种形式:

  • 从前向后索引:这种形式使用非负整数作为索引值,从集合的起始位置开始计数。

    • collection[index]
  • 从后向前索引:这种形式使用负整数作为索引值,从集合的末尾位置开始计数。

    • ^index

下面是一些示例,演示了如何使用 index 运算符访问集合中的元素:

string[] names = { "Alice", "Bob", "Charlie", "Dave", "Eve" };

string name1 = names[2];
Console.WriteLine(name1);  // 输出 "Charlie"

string name2 = names[^1];
Console.WriteLine(name2);  // 输出 "Eve"

在上述示例中,我们创建了一个字符串数组 names,其中包含了几个元素。通过使用 index 运算符,我们可以访问数组中的特定元素。

names[2] 表示正向索引,访问数组中索引为 2 的元素,即第三个元素,输出为 "Charlie"。

names[^1] 表示反向索引,访问数组中索引为 -1 的元素,即最后一个元素,输出为 "Eve"。

需要注意的是,index 运算符的使用要求目标集合实现了 System.Index 结构,例如 System.Range 类型和 System.Index 类型。许多常见的集合类型(如数组、List<T>String)已经实现了这些结构。

1.1.10 空合并运算符

?? 和 ??= 运算符 - Null 合并操作符

C# 空合并运算符(null coalescing operator)用于简化处理可能为 null 的表达式的情况。它表示为 ??

空合并运算符的语法如下:

expression1 ?? expression2

它的行为如下:

  • 如果 expression1 不为 null,则结果为 expression1 的值。

  • 如果 expression1 为 null,则结果为 expression2 的值。

以下是一个使用空合并运算符的示例:

string name = null;
string displayName = name ?? "Unknown";
Console.WriteLine(displayName);  // 输出 "Unknown"

name = "John";
displayName = name ?? "Unknown";
Console.WriteLine(displayName);  // 输出 "John"

在上面的示例中,如果 name 变量为 null,空合并运算符将返回 "Unknown",否则返回 name 的值。

空合并运算符使得代码更加简洁,并且可以避免在处理可能为 null 的表达式时出现 NullReferenceException 异常。

1.1.11 空值条件运算符

成员访问运算符和表达式 | Microsoft Learn

C# 中减少大量代码行的一个功能是空值条件运算符。生产环境中的大量代码行都会验证空值条件。访问作为方法参数传递的成员变量之前,需要检查它,以确定该变量的值是否为 null,否则会抛出一个 NullReferenceException 异常。.NET 设计准则制定,代码不应该抛出这些类型的异常,应该检查空值条件。然而,很容易忘记这样的检查。下面的代码片段验证传递的参数 p 是否为空。如果它为空,方法就只是返回,而不会继续执行:

public void ShowPerson(Person P)
{
    if (p == null) return;
    string firstName = p.FirstName;
}

使用控制条件运算符访问 FirstName 属性,当 p 为空时,就只返回 null,而不继续执行表达式的右侧。

public void ShowPerson(Person P)
{
    string firstName = p?.FirstName;
    // ...
}

使用空值条件运算符访问非可空类型的属性时,不能把结果直接分配给非可空类型,因为结果可以为空。解决这个问题的一种选择是把结果分配给可空的可空类型:

int? age = p?.Age;

当然,要解决这个问题,也可以使用空合并运算符,定义另一个结果,以防止左边的结果为空:

int age1 = p?.Age ?? 0;

也可以结合多个控制条件运算符。下面访问 Person 对象的 Address 属性,这个属性又定义了 City 属性。Person 对象需要进行 null 检查,如果它不为空,Address 属性的结果也不为空:

Person p = GetPerson();
string city = null;
if(p != null && p.HomeAddress != null)
{
    city = p.HomeAddress.City;
}

使用空值条件运算符时,代码会更简单:

string city = p?.HomeAddress?.City;

还可以把空值条件运算符用于数组。下面代码会抛出 NullReferenceException 异常:

int[] arr = null;
int x1 = arr[0];

使用空值条件运算符:

int x1 = arr?[0] ?? 0;

// arr 是一个数组,它可能为 null。
// arr?[0] 使用空值条件运算符 ?. 访问 arr 数组的第一个元素。如果 arr 不为 null,则返回第一个元素的值;如果 arr 为 null,则整个表达式返回 null。
// ?? 是空合并运算符,用于处理可能为 null 的表达式。它表示如果左侧表达式的值为 null,则返回右侧表达式的值。在这种情况下,如果 arr?[0] 的结果为 null,那么整个表达式将返回右侧的值 0。
// int x1 = 是将表达式的结果赋值给变量 x1,并将其声明为整数类型。

// 综合起来,这段代码的意思是:

// 如果 arr 数组不为 null 且其第一个元素也不为 null,则将第一个元素的值赋给变量 x1。
// 如果 arr 数组为 null 或者其第一个元素为 null,则将变量 x1 的值设为 0。

2. 运算符的优先级和关联性

C# 运算符的优先级和关联性_dotNET跨平台的博客-CSDN博客

除了运算符优先级,对于二元运算符,需要注意运算符是从左向右还是从右向左计算。除了少数运算符,所有的二元运算符都是左关联的。

例如:x + y + z;​​

就等于:(x + y) + z;​​

需要先注意运算符的优先级,再考虑其关联性。在以下表达式中,先计算 y 和 z 相乘,再把计算的结果分配给 x,因为乘法的优先级高于加法:

​x + y * z;​​

关联性的重要例外是赋值运算符,它们是右关联。下面的表达式从右向左计算:

​ x = y = z;​​

因为存在右关联性,所有变量 x、y、z 的值都是 3,且该运算符是从右向左计算的。如果这个运算符是从左向右计算,就不会是这种情况:

int z = 3;
int y = 2;
int x = 1;
x = y = z;

一个重要的、可能误导的右关联运算符是条件运算符。

表达式:a ? b : c ? d : e​​

等于:a = b : (c ? d : e)​​

这是因为该运算符是右关联的。

注意:在复杂的表达式中,应避免利用运算符优先级来生成正确的结果。使用圆括号指定运算符的执行顺序,可以使代码更整洁,避免出现潜在的冲突。

3. 使用二进制运算符

3.1 位的移动

在C#中,位移操作用于对二进制位进行移动。C#提供了左移(<<)和右移(>>)两个位移操作符。

左移操作符(<<)将一个数的所有位向左移动指定的位数。移动后,右侧的位将用零填充。语法如下:

result = value << shift;

其中,value 是要进行位移的数值,shift 是要移动的位数,result 是结果。

例如,对于十进制数 5(二进制表示为 101),执行左移操作 5 << 2,将所有位向左移动两位,得到结果 20(二进制表示为 10100)。

右移操作符(>>)将一个数的所有位向右移动指定的位数。移动后,左侧的位将根据操作数的符号进行填充。对于正数,使用零填充;对于负数,使用一填充。语法如下:

result = value >> shift;

同样,value 是要进行位移的数值,shift 是要移动的位数,result 是结果。

例如,对于十进制数 10(二进制表示为 1010),执行右移操作 10 >> 1,将所有位向右移动一位,得到结果 5(二进制表示为 101)。

位移操作在某些情况下很有用,比如对于二进制表示的数字进行乘法和除法的快速计算,或者在位掩码、位标志和位运算中进行操作。

3.2 有符号数和无符号数

在C#中,有符号数和无符号数是用来表示整数的两种不同的数据类型。

有符号数(Signed)是用来表示正数、负数和零的整数。在C#中,有符号数有以下几种类型:

  • sbyte:有符号字节,范围为 -128 到 127。

  • short:有符号短整数,范围为 -32,768 到 32,767。

  • int:有符号整数,范围为 -2,147,483,648 到 2,147,483,647。

  • long:有符号长整数,范围为 -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807。

无符号数(Unsigned)是用来表示非负数和零的整数。在C#中,无符号数有以下几种类型:

  • byte:无符号字节,范围为 0 到 255。

  • ushort:无符号短整数,范围为 0 到 65,535。

  • uint:无符号整数,范围为 0 到 4,294,967,295。

  • ulong:无符号长整数,范围为 0 到 18,446,744,073,709,551,615。

有符号数和无符号数之间的区别在于它们如何解释二进制表示。有符号数使用最高位作为符号位,决定了整数的正负性。而无符号数将所有的位都用于表示数值,没有符号位。

在使用有符号数和无符号数时,需要根据具体的需求和数值范围来选择合适的类型。通常情况下,如果需要表示正数、负数和零,应该使用有符号数;如果只需要表示非负数和零,可以使用无符号数。注意,在进行混合运算或者类型转换时,需要注意有符号数和无符号数之间的差异,以避免出现意外的结果。

4 类型的安全性

4.1 类型转换

C# 支持两种转换方式:隐式转换和显示转换。

4.1.1 隐式转换

只要能确保值不会发生任何变化,类型转换就可以自动(隐式)进行。

注意:只能从较小的整数类型隐式地转换为较大的整数类型,而不能从较大的整数类型隐式地转换为较小的整数类型。也可以在整数和浮点数之间转换;然而,其规则略有不同。尽管可以在相同大小的类型之间转换,如 int/uint 转换为 float,long/ulong 转换为 double,也可以从 long/ulong 转换为 float。这样做可能会丢失 4个字节的数据,但这仅表示得到的 float 值比使用 double 得到的值精度低;编译器认为这是一种可以接受的错误,因为值的数量级不会受到影响。还可以将无符号的变量分配给有符号的变量,只要无符号变量值的大小在有符号变量的范围之内即可。

在隐式地转换值类型时,对于可空类型需要考虑其他因素:

  • 可空类型隐式地转换为其他可空类型应遵循非可空类型的转换规则。

  • 非可空类型隐式地转换为可空类型也要遵循转换规则。

  • 可空类型不能隐式地转换为非可空类型,此时必须进行显示转换。这是因为可空类型的值可以是 null,但非可空类型不能表示这个值。

4.1.2 显式转换

有许多场合不能隐式地转换类型,否则编译器会报告错误。

但是,可以使用类型强制转换显式地执行这些转换。在把一种类型强制转换为另一种类型时,有意地迫使编译器进行转换。类型强制转换的一般语法如下:

long val = 30000;
int i = (int)val;

这表示,把强制转换的目标类型名放在要转换值之前的圆括号中。这种强制类型转换是一种比较危险的操作,即使在从 long 转换为 int 这样简单的类型强制转换过程中,如果原来的 long 的值比 int 的最大值还大,就会出现问题:

long val = 30000000000;
int i = (int)val; // An invalid cast. The maximum int is 2147483647

在本例中,不会报告错误,但也得不到期望的结果。如果运行上面的代码,并将输出结果存储在 i 中,则其值为:-1294967296

最好假定显式类型转换不会给出希望的结果。如前所述,C# 提供了一个 checked 运算符,使用它可以测试操作是否会导致算术溢出。使用 checked 运算符可以检查类型强制转换是否安全,如果不安全,就要迫使运行库抛出一个溢出异常:

long val = 30000000000;
int i = checked((int)val);

记住,所有的显式类型强制转换都可能不安全。

4.2 装箱和拆箱

装箱用于描述把一个值类型转换为引用类型。运行库会为堆上的对象创建一个临时的引用类型“箱子”。

该转换可以隐式地进行,还可以显式地进行转换:

int myIntNumber = 1;
object myObject = myIntNumber;

拆箱用于描述相反的过程,其中以前装箱的值类型强制转换回值类型。这里使用术语“强制转换”,是因为这种类型是显式进行的。其语法类似于前面的显式类型转换:

int myIntNumber = 1;
object myObject = myIntNumber; // Boxing
int mySecondNumber = (int)myObject; // Unboxing

只能对以前装箱的变量进行拆箱,当 myObject 不是装箱的 int 类型时,如果执行最后一行代码,就会在运行期间抛出一个运行时异常。

这里有一个警告:在拆箱时必须非常小心,确保得到的值变量有足够的空间存储拆箱的值中的所有字节。

long myLongNumber = 333333423;
object myObject = (object)myLongNumber;
int myIntNumber = (int)myObject; // InvalidCastException

5. 比较对象的相等性

5.1 比较引用类型的相等性

5.1.1 ReferenceEquals() 方法

ReferenceEquals() 方法是C#中的一个静态方法,用于比较两个对象的引用是否相等。它属于System.Object类,因此可以在任何对象上使用。以下是关于ReferenceEquals()方法的详细介绍:

方法签名:

public static bool ReferenceEquals(object obj1, object obj2)

参数:

  • obj1:要进行比较的第一个对象。

  • obj2:要进行比较的第二个对象。

返回值:

  • 如果obj1obj2引用同一个对象,则返回true

  • 如果obj1obj2引用不同的对象(包括其中一个或两者都为null),则返回false

注意事项:

  • ReferenceEquals() 方法比较的是两个对象的引用,而不是它们的值。即使两个对象具有相同的属性值,如果它们的引用不同,ReferenceEquals() 方法仍然会返回false

  • 如果任何一个参数为null,则ReferenceEquals() 方法将直接返回false,无需进一步比较引用。

  • ReferenceEquals() 方法是一个静态方法,意味着您不需要实例化对象来使用它。您可以直接通过类名调用该方法。

下面是一个使用ReferenceEquals() 方法的示例:

string str1 = "Hello";
string str2 = "Hello";
string str3 = null;

bool result1 = ReferenceEquals(str1, str2);
Console.WriteLine(result1);  // 输出: true

bool result2 = ReferenceEquals(str1, str3);
Console.WriteLine(result2);  // 输出: false

在上述示例中,我们比较了三个字符串对象的引用。str1str2 两个字符串的值相同,因此它们共享同一个字符串常量。ReferenceEquals(str1, str2) 返回 true,表示它们引用同一个对象。而 str1str3 的引用不同,因此 ReferenceEquals(str1, str3) 返回 false

ReferenceEquals() 方法在以下情况下非常有用:

  • 需要检查两个对象是否引用同一个实例。

  • 在需要比较引用而不是值的情况下进行对象比较。

  • 在需要快速检查对象是否为null时。

请注意,ReferenceEquals() 方法在一般情况下不应该用于比较值类型(如整数、浮点数等),因为值类型的比较应使用==运算符或Equals()方法。ReferenceEquals() 方法适用于引用类型的对象比较。

5.1.2 Equals() 虚方法

Equals() 是C#中的一个虚方法,定义在System.Object类中,可以被派生类重写以提供自定义的相等性比较逻辑。以下是关于Equals()虚方法的详细介绍:

方法签名:

public virtual bool Equals(object obj)

参数:

  • obj:要与当前对象进行比较的对象。

返回值:

  • 如果当前对象与obj相等,则返回true

  • 如果当前对象与obj不相等,则返回false

注意事项:

  • Equals() 方法默认情况下执行引用相等性的比较,即两个对象的引用是否相同。这与ReferenceEquals() 方法的行为相同。

  • 派生类可以重写Equals() 方法,以提供自定义的相等性比较逻辑。

  • 重写Equals() 方法时,应根据对象的语义确定相等性的条件。通常情况下,应该比较对象的成员(例如属性、字段)来确定它们的相等性。

  • 重写Equals() 方法时,应遵循一些约定,例如遵循相等性的传递性和一致性原则。

  • 通常,重写Equals() 方法的同时,还需要重写GetHashCode() 方法,以确保相等的对象具有相等的哈希码。

  • 某些类型(例如string和数值类型)已经重写了Equals() 方法,以提供适当的值相等性比较。因此,对于这些类型的实例,可以直接使用Equals() 方法进行比较,而无需进行额外的重写。

下面是一个使用Equals() 方法的示例:

class Person
{
    public string Name { get; set; }

    public override bool Equals(object obj)
    {
        if (obj is Person otherPerson)
        {
            return this.Name == otherPerson.Name;
        }

        return false;
    }
}

Person person1 = new Person { Name = "John" };
Person person2 = new Person { Name = "John" };
Person person3 = new Person { Name = "Jane" };

bool result1 = person1.Equals(person2);
Console.WriteLine(result1);  // 输出: true

bool result2 = person1.Equals(person3);
Console.WriteLine(result2);  // 输出: false

在上述示例中,我们定义了一个Person类,并重写了Equals() 方法来比较Name属性的相等性。如果两个Person对象的Name属性相同,则认为它们相等。

我们使用Equals() 方法进行比较:

  • person1.Equals(person2) 返回 true,因为它们的Name属性值都为 "John"。

  • person1.Equals(person3) 返回 false,因为person1Name属性值为 "John",而person3Name属性值为 "Jane"。

通过重写Equals() 方法,我们可以根据对象的特定语义来定义相等性比较的条件。

5.1.3 静态的 Equals() 方法

在C#中,静态的Equals()方法是一个通用的方法,用于比较两个对象的相等性。它是在System.Object类中定义的静态方法,可以通过类名直接调用,而无需创建对象实例。以下是关于静态的Equals()方法的详细介绍:

方法签名:

public static bool Equals(object objA, object objB)

参数:

  • objA:要进行比较的第一个对象。

  • objB:要进行比较的第二个对象。

返回值:

  • 如果objAobjB相等,则返回true

  • 如果objAobjB不相等,则返回false

注意事项:

  • 静态的Equals()方法不依赖于对象的类型,而是基于参数的类型进行比较。这意味着您可以将不同类型的对象传递给Equals()方法进行比较。

  • 默认情况下,静态的Equals()方法执行引用相等性的比较,即两个对象的引用是否相同。这与ReferenceEquals()方法的行为相同。

  • 派生类可以重写Equals()方法来提供自定义的相等性比较逻辑。如果在比较时涉及到派生类的实例,则将调用派生类重写的Equals()方法。

  • 重写Equals()方法时,应根据对象的语义确定相等性的条件,并使用相应的类型转换或强制转换来比较参数。

  • 对于某些类型(如string和数值类型),它们已经重写了Equals()方法,以提供适当的值相等性比较。因此,对于这些类型的实例,可以直接使用静态的Equals()方法进行比较,而无需进行额外的重写。

下面是一个使用静态的Equals()方法的示例:

string str1 = "Hello";
string str2 = "Hello";
string str3 = "World";

bool result1 = Equals(str1, str2);
Console.WriteLine(result1);  // 输出: true

bool result2 = Equals(str1, str3);
Console.WriteLine(result2);  // 输出: false

在上述示例中,我们使用静态的Equals()方法比较了三个字符串对象的相等性。str1str2两个字符串的值相同,因此它们被认为是相等的。而str1str3的值不同,所以它们被认为是不相等的。

静态的Equals()方法提供了一种更通用的比较方法,可以用于比较任意类型的对象。它对于在编写通用代码时进行对象比较非常有用,特别是当您不确定对象类型时。但需要注意的是,对于某些类型,可能需要重写Equals()方法以提供更精确的相等性比较逻辑。

5.1.4 比较运算符(==)

在C#中,比较运算符==是一种用于比较两个操作数是否相等的运算符。它返回一个布尔值,表示比较的结果。以下是关于比较运算符==的详细介绍:

==

操作数:

  • 左操作数:要进行比较的第一个操作数。

  • 右操作数:要进行比较的第二个操作数。

返回值:

  • 如果左操作数等于右操作数,则返回true

  • 如果左操作数不等于右操作数,则返回false

注意事项:

  • 比较运算符==默认情况下执行的是值相等性的比较,而不是引用相等性的比较。

  • 对于引用类型的对象,默认情况下,==运算符比较的是对象的引用,即两个对象是否指向同一个内存地址。

  • 对于值类型的对象,默认情况下,==运算符比较的是对象的值,即比较对象的各个成员是否相等。

  • 对于字符串类型,==运算符比较的是字符串的内容是否相等,而不是引用。

  • 某些类型(如string和数值类型)已经重载了==运算符,以提供适当的值相等性比较。因此,对于这些类型的实例,可以直接使用==运算符进行比较,而无需进行额外的重载。

下面是一些使用==运算符的示例:

int num1 = 5;
int num2 = 5;
int num3 = 10;

bool result1 = num1 == num2;
Console.WriteLine(result1);  // 输出: true

bool result2 = num1 == num3;
Console.WriteLine(result2);  // 输出: false

string str1 = "Hello";
string str2 = "Hello";
string str3 = "World";

bool result3 = str1 == str2;
Console.WriteLine(result3);  // 输出: true

bool result4 = str1 == str3;
Console.WriteLine(result4);  // 输出: false

在上述示例中,我们使用==运算符比较了不同类型的操作数。对于整数类型,==运算符比较的是操作数的值。因此,num1 == num2 返回true,表示num1等于num2。而num1 == num3 返回false,表示num1不等于num3

对于字符串类型,==运算符比较的是字符串的内容。因此,str1 == str2 返回true,表示str1str2的内容相等。而str1 == str3 返回false,表示str1str3的内容不相等。

需要注意的是,对于自定义类型的对象,比较运算符==的默认行为是比较对象的引用。如果需要对自定义类型进行值相等性的比较,可以重载==运算符,以提供适当的逻辑。

5.2 比较值类型的相等性

在比较值类型的相等性时,采用与引用类型相同的规则:ReferenceEquals() 用于比较引用,Equals()用于比较值,比较运算符可以看作一个中间项。但最大的区别是值类型需要装箱,才能把它们转换为引用,进而才能对它们执行方法。微软已经在 System.ValueType 类中重载了实例方法 Equals(),以便对值类型进行合适的相等性测试。

如果调用 sA.Equals(sB),其中 sA 和 sB 是某个结构的实例,则根据 sA 和 sB 是否在其所有的字段中包含相同的值而返回 true 或 false。另一方面,在默认情况下,不能对自己的结构重载 == 运算符。在表达式中使用 (sA == sB) 会导致一个编译错误,除非在代码中为当前的结构提供了 == 的重载版本。

另外,ReferenceEquals() 在应用于值类型时总是返回 false,因为为了调用这个方法,值类型需要装箱到对象中。每个对象都会被单独装箱,这意味着会得到不同的引用。出于上述原因,调用 ReferenceEquals() 来比较值类型实际上没有什么意义,所以不能调用它。

尽管 System.ValueType 提供的 Equals() 默认重写版本肯定足以应付绝大多数自定义的结构,但仍可以针对自己的结构再次重写它,以提高性能。另外,如果值类型包含作为字段的引用类型,就需要重写Equals(),以便为这些字段提供合适的语义,因为 Equals() 的默认重写版本仅比较它们的地址。

6 运算符重载

6.1 运算符的工作方式

C#中的运算符是用于执行各种操作的符号或关键字。它们用于组合、比较、计算和操作不同类型的数据。下面是一些常见的C#运算符及其工作方式的详细介绍:

  • 算术运算符:

    • +:用于执行加法操作。

    • -:用于执行减法操作。

    • *:用于执行乘法操作。

    • /:用于执行除法操作。

    • %:用于执行取模操作(返回除法的余数)。

算术运算符用于对数字进行基本的数学运算。

  • 关系运算符:

    • ==:用于检查两个操作数是否相等。

    • !=:用于检查两个操作数是否不相等。

    • >:用于检查左操作数是否大于右操作数。

    • <:用于检查左操作数是否小于右操作数。

    • >=:用于检查左操作数是否大于或等于右操作数。

    • <=:用于检查左操作数是否小于或等于右操作数。

关系运算符用于比较两个值之间的关系,并返回一个布尔值。

  • 逻辑运算符:

    • &&:逻辑与运算符,用于执行逻辑与操作。

    • ||:逻辑或运算符,用于执行逻辑或操作。

    • !:逻辑非运算符,用于执行逻辑非操作。

逻辑运算符用于对布尔值进行逻辑操作。

  • 赋值运算符:

    • =:简单赋值运算符,用于将右操作数的值赋给左操作数。

    • +=-=*=/=%=:组合赋值运算符,用于将左操作数与右操作数进行相应的运算,并将结果赋给左操作数。

赋值运算符用于给变量赋值。

  • 位运算符:

    • &:按位与运算符,对两个操作数进行按位与操作。

    • |:按位或运算符,对两个操作数进行按位或操作。

    • ^:按位异或运算符,对两个操作数进行按位异或操作。

    • ~:按位取反运算符,对操作数进行按位取反操作。

    • <<:左移运算符,将操作数的二进制位向左移动指定的位数。

    • >>:右移运算符,将操作数的二进制位向右移动指定的位数。

位运算符用于对二进制数据进行位级操作。

以上只是一些常见的C#运算符,还有其他类型的运算符,如条件运算符(?:)、成员访问运算符(.)、索引运算符([])、类型转换运算符(asistypeof等)等。

6.2 运算符重载的示例:Vector 结构

当你定义一个结构时,可以通过运算符重载为该结构定义自定义的行为。下面是一个示例,展示如何在C#中为Vector结构重载一些常见的运算符:

using System;

struct Vector
{
    public int X { get; }
    public int Y { get; }

    public Vector(int x, int y)
    {
        X = x;
        Y = y;
    }

    // 重载加法运算符(+)
    public static Vector operator +(Vector v1, Vector v2)
    {
        return new Vector(v1.X + v2.X, v1.Y + v2.Y);
    }

    // 重载减法运算符(-)
    public static Vector operator -(Vector v1, Vector v2)
    {
        return new Vector(v1.X - v2.X, v1.Y - v2.Y);
    }

    // 重载乘法运算符(*)- 标量乘法
    public static Vector operator *(Vector v, int scalar)
    {
        return new Vector(v.X * scalar, v.Y * scalar);
    }

    // 重载乘法运算符(*)- 向量点乘
    public static int operator *(Vector v1, Vector v2)
    {
        return v1.X * v2.X + v1.Y * v2.Y;
    }

    // 重载相等运算符(==)
    public static bool operator ==(Vector v1, Vector v2)
    {
        return v1.X == v2.X && v1.Y == v2.Y;
    }

    // 重载不等运算符(!=)
    public static bool operator !=(Vector v1, Vector v2)
    {
        return !(v1 == v2);
    }

    // 重载 ToString() 方法
    public override string ToString()
    {
        return $"({X}, {Y})";
    }
}

class Program
{
    static void Main()
    {
        Vector v1 = new Vector(2, 3);
        Vector v2 = new Vector(4, 5);

        Vector v3 = v1 + v2;
        Console.WriteLine(v3);  // 输出: (6, 8)

        Vector v4 = v2 - v1;
        Console.WriteLine(v4);  // 输出: (2, 2)

        Vector v5 = v1 * 3;
        Console.WriteLine(v5);  // 输出: (6, 9)

        int dotProduct = v1 * v2;
        Console.WriteLine(dotProduct);  // 输出: 23

        Console.WriteLine(v1 == v2);  // 输出: False
        Console.WriteLine(v1 != v2);  // 输出: True
    }
}

在上述示例中,Vector结构表示一个二维向量,包含X和Y坐标属性。通过重载运算符,我们为Vector结构定义了加法运算符(+)、减法运算符(-)、乘法运算符(*)(标量乘法和点乘)、相等运算符(==)和不等运算符(!=)。

通过运算符重载,我们可以使用自定义的语法来执行向量的数学运算,并可以直接使用==!=运算符来比较向量的相等性。

输出结果显示了通过运算符重载定义的自定义行为的效果。

6.3 比较运算符的重载

在C#中,可以通过运算符重载为自定义类型重载比较运算符。比较运算符包括相等运算符(==)和不等运算符(!=)。下面是一个示例,展示如何为自定义类型Person重载相等运算符和不等运算符:

using System;

class Person
{
    public string Name { get; set; }
    public int Age { get; set; }

    public Person(string name, int age)
    {
        Name = name;
        Age = age;
    }

    // 重载相等运算符(==)
    public static bool operator ==(Person person1, Person person2)
    {
        if (ReferenceEquals(person1, person2))
            return true;

        if (person1 is null || person2 is null)
            return false;

        return person1.Name == person2.Name && person1.Age == person2.Age;
    }

    // 重载不等运算符(!=)
    public static bool operator !=(Person person1, Person person2)
    {
        return !(person1 == person2);
    }

    // 重写 Equals 方法
    public override bool Equals(object obj)
    {
        if (obj is Person person)
        {
            return this == person;
        }

        return false;
    }

    // 重写 GetHashCode 方法
    public override int GetHashCode()
    {
        return Name.GetHashCode() ^ Age.GetHashCode();
    }
}

class Program
{
    static void Main()
    {
        Person person1 = new Person("Alice", 25);
        Person person2 = new Person("Bob", 30);
        Person person3 = new Person("Alice", 25);

        Console.WriteLine(person1 == person2);  // 输出: False
        Console.WriteLine(person1 != person2);  // 输出: True

        Console.WriteLine(person1 == person3);  // 输出: True
        Console.WriteLine(person1 != person3);  // 输出: False

        Console.WriteLine(person1.Equals(person2));  // 输出: False
        Console.WriteLine(person1.Equals(person3));  // 输出: True
    }
}

在上述示例中,我们为Person类重载了相等运算符(==)和不等运算符(!=)。在相等运算符重载中,我们首先使用ReferenceEquals方法检查两个对象是否引用同一个实例,然后比较两个对象的属性来确定它们是否相等。在不等运算符重载中,我们只需对相等运算符的结果取反即可。

为了使比较运算符能够正常工作,我们还重写了Equals方法和GetHashCode方法。在Equals方法中,我们使用相等运算符来实现相等性的比较。在GetHashCode方法中,我们对NameAge属性的哈希码进行异或运算来生成哈希码。

7. 实现自定义的索引运算符

public class Person
{
    public Person(DateTime birthday, string firstName, string lastName)
    {
        Birthday = birthday;
        FirstName = firstName;
        LastName = lastName;
    }

    public DateTime Birthday { get; }
    public string FirstName { get; }
    public string LastName { get; }
    public override string ToString() => $"{FirstName} {LastName}";
}

为了允许使用索引器语法访问 PersonCollection 并返回 Person 对象,可以创建一个索引器。索引器看起来非常类似于属性,因为它也包含 get 和 set 访问器。两者的不同之处是名称。指定索引器要使用 this 关键字。this 关键字后面的括号指定索引使用的类型。数组提供 int 类型的索引器,所以这里使用 int 类型直接把信息传递给被包含的数组 m_Peoples。get 和 set 访问器的使用非常类似于属性。检索值时调用 get 访问器,在右边传递 Person 对象时调用 set 访问器。

public class PersonCollection
{
    Person[] m_Peoples;
    public PersonCollection(params Person[] people) => m_Peoples = people.ToArray();

    public Person this[int index]
    {
        get => m_Peoples[index];
        set => m_Peoples[index] = value;
    }
}

对于索引器,不能仅定义 int 类型作为索引类型。任何类型都是有效的。

public class PersonCollection
{
    // 这个索引器用来返回有指定生日的每个人。因为多个人员可以有相同的生日,所以不是返回一个 Person 对象,而是用接口 IEnumerable<Person> 返回一个 Person 对象列表
    public IEnumerable<Person> this[DateTime birthDay]
    {
        get => m_Peoples.Where(p => p.Birthday == birthDay);
    }
}

示例:

void Awake()
{
    var p1 = new Person("A", "AA", new DateTime(1961, 1, 21));
    var p2 = new Person("B", "BB", new DateTime(1962, 2, 22));
    var p3 = new Person("C", "CC", new DateTime(1963, 3, 23));
    var p4 = new Person("D", "DD", new DateTime(1964, 4, 24));
    var coll = new PersonCollection(p1, p2, p3, p4);
    Debug.Log(coll[2]); // C CC
    foreach (Person person in coll[new DateTime(1961, 1, 21)])
    {
        Debug.Log(person); // A AA
    }
}

8. 用户定义的类型强制转换

C# 允许定义自己的数据类型(结构或类),这意味着需要某些工具支持在自定义的数据类型之间进行类型强制转换。方法是把类型强制转换运算符定义为相关类的一个成员运算符。类型强制转换运算符必须标记为隐式或显式,以说明希望如何使用它。我们应遵循与预定义的类型强制转换相同的指导原则:如果知道无论在源变量中存储什么值,类型强制转换总是安全的,就可以把它定义为隐式强制转换。然而,如果某些数值可能会出错,如丢数据或抛出异常,就应把数据类型转换定义为显式强制转换。

定义类型强制转化的语法类似于本章前面介绍的重载运算符。这并不是偶然现象,类型强制转换在某些情况下可以看为一种运算符,其作用是从源类型转换为目标类型。

public static implicit operator float (Currency value){ // processing }

运算符的返回类型定义了类型强制转换操作的目标类型,它有一个参数,即要转换的源对象。这里定义的类型强制转换可以隐式地把 Currency 型的值转换为 float 型。注意,如果数据类型转换声明为隐式,编译器就可以隐式或显式地使用这个转换。如果数据类型转换声明为显式,编译器就只能显式地使用它。与其他运算符重载一样,类型强制转换必须同时声明为 public 和 static。

8.1 实现用户定义的类型强制转换

public struct Currency
{
    public Currency(uint dollars, ushort cents)
    {
        Dollars = dollars;
        Cents = cents;
    }
    public uint Dollars { get; } // 无符号数据类型,确保 Currency 实例只能包含正值。
    public ushort Cents { get; } // 无符号数据类型,确保 Currency 实例只能包含正值。
    public override string ToString() => $"${Dollars}.{Cents,-2:00}";
}
var balance = new Currency(10, 50);
float f = balance; // error: Cannot convert source type 'Currency' to target type 'float'

为此,需要定义一种类型强制转换。

public struct Currency
{
    // 隐式从 Currency 转换到 float
    public static implicit operator float(Currency c) => c.Dollars + c.Cents / 100.0f;
}

这种类型强制转换是隐式的。

注意:这里有一点欺骗性:实际上,当把 uint 转换为 float 时,精确度会丢失,但微软认为这种错误并不重要,因此把从 uint 到 float 的类型强制转换都当作隐式转换。

但是,如果把 float 型转换为 Currency 型,就不能保证转换肯定成功了。float 型可以存储负值,而 Currency 实例不能,且 float 型存储数值的数量级要比 Currency 型的 (uint)Dollar 字段大得多。所以,如果 float 型包含一个不合适的值,把它转换为 Currency 型就会得到意想不到的结果。因此,从 float 型转换到 Currency 型就应定义为显式转换。

public struct Currency
{
    // 显式从 float 转换到 Currency
    public static explicit operator Currency(float value)
    {
        uint dollars = (uint)value;
        ushort cents = (ushort)((value - dollars) * 100);
        return new Currency(dollars, cents);
    }
}
float amount = 45.63f;
Currency amount2 = (Currency)amount;
float amount = 45.63f;
Currency amount2 = amount; // Cannot convert source type 'float' to target type 'Currency', 因为它试图隐式地使用一个显式的类型强制转换

测试:

public static void Main()
{
    try
    {
        Currency balance = new Currency(50, 35);
        Debug.Log(balance);                 // 50.35
        Debug.Log($"balance is {balance}"); // balance is $50.35
        float balance2 = balance;
        Debug.Log($"After converting to float, = {balance2}"); // After converting to float, = 50.35
        balance = (Currency)balance2;
        Debug.Log($"After converting back to Currency, = {balance}"); // After converting back to Currency, = $50.34
        Debug.Log
        (
            "Now attempt to convert out of range value of -$50.50 to a Currency:"
            // Now attempt to convert out of range value of -$50.50 to a Currency:
        );
        checked
        {
            balance = (Currency)(-50.50);
            Debug.Log($"Result is {balance}"); // Result is $4294967246.00
        }
    } catch (Exception e)
    {
        Debug.Log($"Exception occured: {e.Message}");
    }
}

首先,float -> Currency 得到一个错误的结果 50.34,而不是 50.35。其次,在试图转换明显超出范围的值时,没有生成异常。

第一个问题是有舍入错误引起的。如果类型强制转换用于把 float 值转换为 uint 值,计算机就会截取多余的数字,而不是执行四舍五入。计算机以二进制而非十进制方式存储数字,小数部分 0.35 不能用二进制小数来精切表示。所以计算机最后存储了一个略小于 0.35 的值,它可以用二进制格式精确地表示。把该数字乘以 100,就会得到一个小于 35 的数字,它截去了 34 美分。显然在本例中,这种由截去引起的错误是很严重的。避免该错误的方式是确保在数字转换过程中执行只能的四舍五入操作。

幸运的是,Microsoft 编写了一个类 System.Convert 来完成该任务。System.Convert 对象包含大量的静态方法来完成各种数字转换,我们需要使用的是 Convert.ToUInt16(),注意,在使用 System.Convert 类的方法时会造成额外的性能损失,所以只应在需要时使用它们。

下面看看为什么没有抛出期望的溢出异常。此处的问题是溢出异常实际发生的位置根本不在 Awake() 中——它在强制转换运算符的代码中发生的,该代码在 Awake() 中调用,而且没有标记为 checked.

其解决方法是确保类型强制转换本身也在 checked 环境下进行。

public static explicit operator Currency(float value)
{
    checked
    {
        uint dollars = (uint)value;
        ushort cents = Convert.ToUInt16((value - dollars) * 100);
        return new Currency(dollars, cents);
    }
}

注意:如果定义了一种使用非常频繁的类型强制转换,其性能也非常好,就可以不进行任何错误检查。如果对用户定义的类型强制转换和没有检查的错误进行了清晰的说明,这也是一种合理的解决方案。

8.1.1 类之间的类型强制转换

Currency 示例仅涉及与 float(一种预定义的数据类型)来回转换的类。但类型转换不一定会涉及任何简单的数据类型。定义不同结构或类的实例之间的类型强制转换是完全合法的,但有两点限制:

  • 如果某个类派生自另一个类,就不能定义这两个类之间的类型强制转换(这些类型的强制转换已经存在)。

  • 类型强制转换必须在源数据类型或目标数据类型的内部定义。

假定有以下的类层次结构:System Object <- A <- B,B <- C,B <- D

换言之,类 C 和 D 间接派生于 A。在这种情况下,在 A、B、C 或 D 之间唯一合法的自定义类型强制转换就是类 C 和 D 之间的转换,因为这些类并没有互相派生。对应的代码如下所示(假定希望类型强制转换是显式的,这是在用户定义的类之间定义类型强制转换的通常操作):

public static explicit operator D (C value) {}
public static explicit operator C (D value) {}

对于这些类型强制转换,可以选择放置定义的地方——在 C 的类定义内部,或者在 D 的类定义内部,但不能在其他地方定义。C# 要求把类型强制转换的定义放在源类(或结构)或目标类(或结构)的内部。这一要求的副作用是不能定义两个类之间的类型强制转换,除非至少可以编辑其中一个类的源代码。这是因为,这样可以防止第三方把类型强制转换引入类中。

一旦在一个类的内部定义了类型强制转换,就不能在另一个类中定义相同的类型强制转换。显然,对于每一种转换只能有一种类型强制转换,否则编译器就不知道该选择哪个类型强制转换。

8.1.2 基类和派生类之间的类型强制转换

要了解这些类型强制转换是如何工作的,首先看看源和目标数据类型都是引用类型的情况。

MyDerived derived = new MyDerived();
MyBase myBase = derived;
// 子类 子类对象 = new 子类();
// 父类 父类对象 = 子类对象;

在本例中,是从 MyDerived 隐式地强制转换为 MyBase。这是可行的,因为对类型 MyBase 的任何引用都可以引用 MyBase 类的对象或派生自 MyBase 的对象。在 OO 编程中,派生类的实例实际上是基类的实例,但加入了一些额外的信息。在基类上定义的所有函数和字段也都在派生类上得到定义。

MyBase derivedObject = new MyDerived();
MyBase baseObject = new MyBase();
MyDerived derived1 = (MyDerived)derivedObject; // OK
MyDerived derived2 = (MyDerived)baseObject; // Throws exception
// 父类 子类对象 = new 子类();
// 父类 父类对象 = new 父类();
// 子类 子类对象1 = (子类)子类对象;
// 子类 子类对象2 = (子类)父类对象;

对于最后一条语句,如果这个对象可能是要强制转换的派生类的一个实例,强制转换就会成功,派生的引用设置为引用这个对象。但如果该对象不是派生类(或者派生于这个类的其他类)的一个实例,强制转换就会失败,并抛出一个异常。

如果实际上要把 MyBase 实例转换为真实的 MyDerived 对象,该对象的值根据 MyBase 实例的内容来确定,就不能使用类型强制转换语法。最合适的选项通常是定义一个派生类的构造函数,它以基类的实例作为参数,让这个构造函数完成相关的初始化:

class DerivedClass : BaseClass
{
    public DerivedClass(BaseClass base)
    {
        // initialize object from the Base instance
    }
}
#知识点##技术分享##学习笔记##学习##CSharp#
C# 知识库 文章被收录于专栏

C#知识库用于学习和复习C#知识~

全部评论

相关推荐

09-25 10:34
东北大学 Java
多面手的小八想要自然醒:所以读这么多年到头来成为时代车轮底下的一粒尘
点赞 评论 收藏
分享
1 2 评论
分享
牛客网
牛客企业服务