【C#本质论 十】合式类型(一)重写Object成员及操作符重载(上)

简介: 【C#本质论 十】合式类型(一)重写Object成员及操作符重载(上)

第一次看到这章的标题有点懵,啥是合式类型,是一种值类型和引用类型之外的类型么,以前也没有听说过呀?其实并不是,合式类型其实说白了就是合适的类型,如何定义类型,如何操作类型才更好,如何创建合适的值类型和引用类型

这一章的内容比较杂,基本上类似于基础部分的终结之章,回顾下之前学习的章节,1-5章介绍了结构性编程的基础知识,6-10章来介绍面向对象的内容,加上接下来11章对异常处理的延伸学习后,基本内容部分相当于结束了!

重写Object的成员

回顾一下万类始祖的Object所具有的虚方法,除了Finalize方法不能直接调用以外,我们有三个虚方法可以用来重写,用来比较对象的Equals和GetHashCode,以及用于返回字符串的ToString

重写ToSring

为啥要重写ToString,因为Object提供的默认ToString方法提供的是对当前类型的完全限定名输出,这个完全没有任何意义啊,我输出这个对象的字符串信息是想知道一些有用的信息,所以一定要重写。

例如我想输出这个坐标对象的具体坐标,就要通过重写的方式来获得:

重写ToString需要注意以下几条原则:

  • 如需返回有用的、面向开发人员的诊断字符串,就要重写ToString()
  • 要使ToString()返回的字符串简短。
  • 不要从ToString()返回空字符串来代表“空”(null)。
  • 避免ToString()引发异常或造成可观察到的副作用(改变对象状态)
  • 如果返回值与语言文化相关或要求格式化(例如DateTime),就要重载ToString(string format)或实现IFormattable。
  • 考虑从ToString()返回独一无二的字符串以标识对象实例。

总而言之就是要返回有用信息并且千万别在重写的方法里抛异常

重写GetHashCode()

GetHashCode方法是用来获取和对象对应的哈希码,有两种情况必须重写该方法:

  • 重写Equals()方法一定要重写GetHashCode(),否则编译器会有警告
  • 将类作为hash表集合的键使用的时候也要重写GetHashCode()。

要获得良好的性能实现需要参照以下重写规则(“必须”是指必须满足的要求,“性能”是指为了增强性能而需要采取的措施,“安全性”是指为了保障安全性而需要采取的措施):

)。

  • 必须相等的对象必然有相等的哈希码(若a.Equals(b),则a.GetHashCode()==b.GetHashCode())。也就是相等的哈希码是对象相等性的必要不充分条件,对象相等则哈希码一定相等,哈希码相等对象不一定相等,还需要其它条件来满足
  • 必须:在特定对象的生存期内,GetHashCode()始终返回相同的值,即使对象的数据发生了改变。许多时候应缓存方法的返回值,从而确保这一点。
  • 必须GetHashCode()不应引发任何异常;GetHashCode()总是成功返回一个值。
  • 性能哈希码应尽可能唯一。但由于哈希码只是返回一个int,所以只要一种对象包含的值比一个int能够容纳得多(这就几乎涵盖所有类型了),那么哈希码肯定存在重复。一个很容易想到的例子是long,因为long的取值范围大于int,所以假如规定每个int值都只能标识一个不同的long值,那么肯定剩下大量long值没法标识。
  • 性能可能的哈希码值应当在int的范围内平均分布。例如,创建哈希码时如果没有考虑到字符串在拉丁语言中的分布主要集中在初始的128个ASCII字符上,就会造成字符串值的分布非常不平均,所以不能算是好的GetHashCode()算法。
  • 性能:GetHashCode()的性能应该优化。GetHashCode()通常在Equals()实现中用于“短路”一次完整的相等性比较(哈希码都不同,自然没必要进行完整的相等性比较了)。所以,当类型作为字典集合中的键类型使用时,会频繁调用该方法
  • 性能两个对象的细微差异应造成哈希值的极大差异。理想情况下,1 bit的差异应造成哈希码平均16 bits的差异。这有助于确保不管哈希表如何对哈希值进行“装桶”(bucketing),也能保持良好的平衡性。
  • 安全性:攻击者应难以伪造具有特定哈希码的对象。攻击手法是向哈希表中填写大量哈希为同一个值的数据。如哈希表的实现不高效,就易于受到DOS(拒绝服务)攻击。

补充说明一点:高效的哈希表实现就是指哈希值可以良好的均匀的随机分布。

namespace AddisonWesley.Michaelis.EssentialCSharp.Chapter10.Listing10_02
{
    public struct Coordinate
    {
        public Coordinate(Longitude longitude, Latitude latitude)
        {
            Longitude = longitude;
            Latitude = latitude;
        }
        public Longitude Longitude { get; }
        public Latitude Latitude { get; }
        public override int GetHashCode()
        {
            int hashCode = Longitude.GetHashCode();
            // As long as the hash codes are not equal
            if(Longitude.GetHashCode() != Latitude.GetHashCode())
            {
                hashCode ^= Latitude.GetHashCode();  // eXclusive OR
            }
            return hashCode;
        }
        public override string ToString()
        {
            return string.Format("{0} {1}", Longitude, Latitude);
        }
    }
    public struct Longitude { }
    public struct Latitude { }
}

这里使用了异或运算,如果a、b两个值不相同,则异或结果为1。如果a、b两个值相同,异或结果为0。

  • 首先向来自相关类型的哈希码应用XOR操作符,并确保操作数不相近或相等(否则结果全零)
  • 在操作数相近或相等的情况下,考虑改为使用移位(bit shift)和加法(add)操作。其他备选的操作符——AND和OR——具有类似的限制,但这些限制会发生得更加频繁。多次应用AND,会逐渐变成全为0;而多次应用OR,会逐渐变成全为1。

这里的Longitude和Latitude都是只读自动属性,所以值不会变,如果是会发生改变的值,则应该对哈希码进行缓存,来满足生命周期内哈希码的唯一性原则

重写Equals()

重写Equal和重写GetHashCode有一些区别,这要从对象同一性相等的对象值说起:

  • 两个引用假如引用同一个实例,就说这两个引用是同一的。object(因而延展到所有派生类型)提供名为**ReferenceEquals()**的静态方法来显式检查对象同一性
  • 引用同一性只是“相等性”的一个例子。两个对象实例的成员值部分或全部相等,也可以说它们相等

也就是同一个引用只是对象相等的一部分例子,两个对象实例的成员值部分或全部相等,也可以说它们相等。

例如重写Equals方法后,就可以认为对象相等:

public class Program
    {
        public static void Main()
        {
            TML tml1 = new TML("PV", "1000", "09187234");
            TML tml2 = tml1;
            TML tml3 = new TML("PV", "1000", "09187234");
            // 对象是不是引用同一
            if (!TML.ReferenceEquals(tml1, tml2))
            {
                throw new Exception("serialNumber1 does NOT " + "reference equal serialNumber2");
            }
            // 不引用同一总相等吧
            else if (!tml1.Equals(tml2))
            {
                throw new Exception("serialNumber1 does NOT equal serialNumber2");
            }
            else
            {
                Console.WriteLine(
                    "serialNumber1 reference equals serialNumber2");
                Console.WriteLine(
                    "serialNumber1 equals serialNumber2");
            }
            // 对象是不是引用同一
            if (TML.ReferenceEquals(tml1, tml3))
            {
                throw new Exception("serialNumber1 DOES reference " + "equal serialNumber3");
            }
            // 不引用同一总相等吧
            else if (!tml1.Equals(tml3) ||tml1!= tml3)
            {
                throw new Exception("serialNumber1 does NOT equal serialNumber3");
            }
            Console.WriteLine("serialNumber1 equals serialNumber3");
        }
    }
    public class TML
    {
        public TML(string name, string price, string number)
        {
            Name = name;
            Price = price;
            Number = number;
        }
        public string Name { get; }
        public string Price { get; }
        public string Number { get; }
    }

输出如下:

serialNumber1 reference equals serialNumber2
serialNumber1 equals serialNumber2
serialNumber1 equals serialNumber3

其中serialNumber1 和serialNumber2是引用同一,serialNumber1 和serialNumber3是重写Equals方法和!=操作符后的相等性验证通过,这个验证接下来会说到。注意这里的serialNumber1 和serialNumber3相等需要的场景也有很多,很多时候可以用于查重,如果不通过重写的方式验证相等则只能认定引用同一才是相等,这样通过不同方式创建的数据就都能逃过查重检验了。这里需要注意的两点:

  • 只有引用类型才能使用ReferenceEquals方法判断,值类型的调用永远是false,因为值类型要调用该方法一定要装箱为object,而各自装箱产生的引用一定不是同一个。
  • Object.Equals()的实现只是简单调用了一下ReferenceEquals方法,所以用处很有限。大多数情况下需要重写。
相关文章
|
3月前
|
C#
一文搞懂C#中类成员的可访问性
一文搞懂C#中类成员的可访问性
52 5
|
19天前
|
编译器 C#
c# - 运算符<<不能应用于long和long类型的操作数
在C#中,左移运算符的第二个操作数必须是 `int`类型,因此需要将 `long`类型的位移计数显式转换为 `int`类型。这种转换需要注意数据丢失和负值处理的问题。通过本文的详细说明和示例代码,相信可以帮助你在实际开发中正确使用左移运算符。
27 3
|
18天前
|
编译器 C#
c# - 运算符<<不能应用于long和long类型的操作数
在C#中,左移运算符的第二个操作数必须是 `int`类型,因此需要将 `long`类型的位移计数显式转换为 `int`类型。这种转换需要注意数据丢失和负值处理的问题。通过本文的详细说明和示例代码,相信可以帮助你在实际开发中正确使用左移运算符。
34 1
|
16天前
|
编译器 C#
c# - 运算符<<不能应用于long和long类型的操作数
在C#中,左移运算符的第二个操作数必须是 `int`类型,因此需要将 `long`类型的位移计数显式转换为 `int`类型。这种转换需要注意数据丢失和负值处理的问题。通过本文的详细说明和示例代码,相信可以帮助你在实际开发中正确使用左移运算符。
10 0
|
1月前
|
C#
C# 可空类型(Nullable)
C# 单问号 ? 与 双问号 ??
46 12
|
1月前
|
Python
通过 type 和 object 之间的关联,进一步分析类型对象
通过 type 和 object 之间的关联,进一步分析类型对象
54 3
|
3月前
|
存储 C#
揭秘C#.Net编程秘宝:结构体类型Struct,让你的数据结构秒变高效战斗机,编程界的新星就是你!
【8月更文挑战第4天】在C#编程中,结构体(`struct`)是一种整合多种数据类型的复合数据类型。与类不同,结构体是值类型,意味着数据被直接复制而非引用。这使其适合表示小型、固定的数据结构如点坐标。结构体默认私有成员且不可变,除非明确指定。通过`struct`关键字定义,可以包含字段、构造函数及方法。例如,定义一个表示二维点的结构体,并实现计算距离原点的方法。使用时如同普通类型,可通过实例化并调用其成员。设计时推荐保持结构体不可变以避免副作用,并注意装箱拆箱可能导致的性能影响。掌握结构体有助于构建高效的应用程序。
111 7
|
3月前
|
程序员 C#
C# 语言类型全解
C# 语言类型全解
25 0
|
4月前
|
SQL 安全 Java
Android经典面试题之Kotlin中object关键字实现的是什么类型的单例模式?原理是什么?怎么实现双重检验锁单例模式?
Kotlin 单例模式概览 在 Kotlin 中,`object` 关键字轻松实现单例,提供线程安全的“饿汉式”单例。例如: 要延迟初始化,可使用 `companion object` 和 `lazy` 委托: 对于参数化的线程安全单例,结合 `@Volatile` 和 `synchronized`
59 6
|
3月前
|
开发框架 .NET 编译器
C# 中的记录(record)类型和类(class)类型对比总结
C# 中的记录(record)类型和类(class)类型对比总结