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

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

了解了引用同一性和想等性我们来看看重写Equals的步骤吧:

  1. 检查是否为null--------不为null才能继续哦,否则没有比较的必要
  2. 如果是引用类型,就检查引用是否相等------引用同一则一定相等
  3. 检查数据类型是否相同
  4. 调用一个指定了具体类型的辅助方法,它的操作数是具体要比较的类型而不是object(例如代码清单10.5中的Equals(Coordinate obj)方法)
  5. 可能要检查哈希码是否相等来短路一次全面的、逐字段的比较---------相等的两个对象不可能哈希码不同,哈希码不同则一定不等,但是有时候散列不均匀或者没有缓存,则可能导致返回的hash值并非独一无二,所以不能依赖它判断亮哥对象是否相等。
  6. 如基类重写了Equals(),就检查base.Equals()
  7. 比较每一个标识字段(关键字段),判断是否相等
  8. 重写GetHashCode()
  9. 重写==和!=操作符(参见下一节)

可以通过如下代码实现来验证步骤:

namespace AddisonWesley.Michaelis.EssentialCSharp.Chapter10.Listing10_05
{
    using System;
    public class Program
    {
        public static void Main()
        {
            //...
            Coordinate coordinate1 =
                new Coordinate(new Longitude(48, 52),
                                new Latitude(-2, -20));
            // Value types will never be reference equal
            if(Coordinate.ReferenceEquals(coordinate1,
                coordinate1))
            {
                throw new Exception(
                    "coordinate1 reference equals coordinate1");
            }
            Console.WriteLine(
                "coordinate1 does NOT reference equal itself");
        }
    }
    public struct Coordinate : IEquatable<Coordinate>
    {
        public Coordinate(Longitude longitude, Latitude latitude)
        {
            Longitude = longitude;
            Latitude = latitude;
        }
        public Longitude Longitude { get; }
        public Latitude Latitude { get; }
        public override bool Equals(object obj)
        {
            // STEP 1: Check for null
            if (obj == null)
            {
                return false;
            }
            // STEP 3: Equivalent data types
            if (this.GetType() != obj.GetType())
            {
                return false;
            }
            return Equals((Coordinate)obj);
        }
        public bool Equals(Coordinate obj)
        {
            // STEP 1: Check for null if a reference type
            // (e.g., a reference type)
            // if (obj == null)
            // {
            //     return false;
            // }
            // STEP 2: Check for ReferenceEquals if this 
            // is a reference type.
            // if ( ReferenceEquals(this, obj))
            // {
            //   return true;
            // }
            // STEP 4: Possibly check for equivalent hash codes.
            // if (this.GetHashCode() != obj.GetHashCode())
            // {
            //    return false;
            // } 
            // STEP 5: Check base.Equals if base overrides Equals().
            // System.Diagnostics.Debug.Assert(
            //     base.GetType() != typeof(object) );
            // if ( !base.Equals(obj) )
            // {
            //    return false;
            // } 
            // STEP 6: Compare identifying fields for equality
            // using an overload of Equals on Longitude
            return ((Longitude.Equals(obj.Longitude)) &&
                (Latitude.Equals(obj.Latitude)));
        }
        // STEP 7: Override GetHashCode
        public override int GetHashCode()
        {
            int hashCode = Longitude.GetHashCode();
            hashCode ^= Latitude.GetHashCode(); // Xor (eXclusive OR)
            return hashCode;
        }
        public static bool operator ==(
            Coordinate leftHandSide,
            Coordinate rightHandSide)
        {
            return (leftHandSide.Equals(rightHandSide));
        }
        public static bool operator !=(
            Coordinate leftHandSide,
            Coordinate rightHandSide)
        {
            return !(leftHandSide.Equals(rightHandSide));
        }
    }
    public struct Longitude
    {
        public Longitude(int x, int y) { }
    }
    public struct Latitude
    {
        public Latitude(int x, int y) { }
    }
}

该实现的前两个检查很容易理解。但注意如果类型密封,步骤3可以省略,步骤4~6在Equals()的一个重载版本中进行,它获取Coordinate类型的对象作为参数。这样在比较两个Coordinate对象时,就可完全避免执行Equals(object obj)及其GetType()检查。要注意如下设计规范:

  • 要一起实现GetHashCode()、Equals()、==操作符和!=操作符,缺一不可
  • 要用相同算法实现Equals()、==和!=
  • 避免在GetHashCode()、Equals()、==和!=的实现中引发异常
  • 避免在可变引用类型(也就是Object)上重载相等性操作符(如重载的实现速度过慢,也不要重载)。
  • 要在实现IComparable时实现与相等性相关的所有方法。

说了这么多,实际上重写Equal就是为了达到标识数据相等的目的。这里为啥不用GetHashCode,因为这里没有缓存哦,所以不能百分百保证相等的对象就一定返回相等的hash,有可能因为缓存没有处理好导致其不等,实际上却是相等的。

用元组重写GetHashCode()和Equals()

其实GetHashCode()和Equals()的主要作用就是克服Object呆板简单的判断,但是实现起来却很繁琐,需要对所有关键标识数据进行操作。对于Equals(Coordinate coordinate),可将每个标识(关键)成员合并到一个元组中,并将它们和同类型的目标实参比较:

public class TML : IEquatable<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; }
        public bool Equals(TML tml)
        {
            return (Name, Price, Number).Equals((tml.Name, tml.Price, tml.Number));
        }
        public override int GetHashCode()
        {
            return (Name, Price, Number).GetHashCode();
        }
    }

使用元组,所有的底层实现都由元组搞定,只需要标识用来比较的关键成员信息就行了。

操作符重载

实现操作符的过程称为操作符重载,不仅仅包括==和!=,还支持一些其它操作符,当然在使用的时候需要注意以下两点,防止出现误操作:

  • 赋值运算符=不支持重载
  • 重载的操作符不能通过IntelliSense呈现,也就是不能智能提示。
  • ==默认也只是执行引用相等性检查,所以为了保证和Equals的同一性,一定也要重写该操作符!

对于==和!=操作符而言,其操作行为可以直接委托给Equals:

==和!=重载

public sealed class ProductSerialNumber
    {
        public ProductSerialNumber(
            string productSeries, int model, long id)
        {
            ProductSeries = productSeries;
            Model = model;
            Id = id;
        }
        public string ProductSeries { get; }
        public int Model { get; }
        public long Id { get; }
        public bool Equals(ProductSerialNumber obj)
        {
            return ((obj != null) && (ProductSeries == obj.ProductSeries) && (Model == obj.Model) && (Id == obj.Id));
        }
        public static bool operator ==(
            ProductSerialNumber leftHandSide,
            ProductSerialNumber rightHandSide)
        {
            if (ReferenceEquals(leftHandSide, null))
            {
                return ReferenceEquals(rightHandSide, null);
            }
            return (leftHandSide.Equals(rightHandSide));
        }
        public static bool operator !=(
            ProductSerialNumber leftHandSide,
            ProductSerialNumber rightHandSide)
        {
            return !(leftHandSide == rightHandSide);
        }
    }

这里需要注意的是,一定不要用相等性操作符执行空检查(leftHandSide==null)。否则会递归调用方法,造成只有栈溢出才会终止的死循环。相反,应调用ReferenceEquals()检查是否为空。

+和-重载

定义两个对象之间的+和-实际上也就是定义其关键数据的+和-:

public struct Coordinate
    {
        public Coordinate(Longitude longitude, Latitude latitude)
        {
            Longitude = longitude;
            Latitude = latitude;
        }
        public static Coordinate operator +(
            Coordinate source, Arc arc)
        {
            Coordinate result = new Coordinate(
                new Longitude(
                    source.Longitude + arc.LongitudeDifference),
                new Latitude(
                    source.Latitude + arc.LatitudeDifference));
            return result;
        }
        public static Coordinate operator -(
            Coordinate source, Arc arc)
        {
            Coordinate result = new Coordinate(
                new Longitude(
                    source.Longitude - arc.LongitudeDifference),
                new Latitude(
                    source.Latitude - arc.LatitudeDifference));
            return result;
        }
  }

转型操作符

怎么将值类型转换为一个不相干的引用类型呢?或者将值类型转换为一个不相干的结构,这需要定义一个转换器,例如转换double类型和高度类型Latitude:

public struct Latitude
    {
        public Latitude(double decimalDegrees)
        {
            DecimalDegrees = Normalize(decimalDegrees);
        }
        public double DecimalDegrees { get; }
        // ...
        public static implicit operator double(Latitude latitude)
        {
            return latitude.DecimalDegrees;
        }
        public static implicit operator Latitude(double degrees)
        {
            return new Latitude(degrees);
        }
        private static double Normalize(double decimalDegrees)
        {
            // here you would normalize the data
            return decimalDegrees;
        }
    }

说白了就是通过方法来换值,但是有一个需要注意的就是转换操作符implicit operator(隐式转换)explicit operator(显式转换),和之前的规范一样,如果判断是有损转换,一定声明为显式的,提醒操作者可能的精度丢失

小小的总结一下,其实本章这两部分内容都是围绕着优化现有Object以及C#提供的操作符的优化,大多数时候其它封装类都定义好了这些,但是我们需要知道,转换是怎么做的,有什么好的方式。明白原理!

相关文章
|
14天前
|
C#
一文搞懂C#中类成员的可访问性
一文搞懂C#中类成员的可访问性
30 5
|
15天前
|
存储 C#
揭秘C#.Net编程秘宝:结构体类型Struct,让你的数据结构秒变高效战斗机,编程界的新星就是你!
【8月更文挑战第4天】在C#编程中,结构体(`struct`)是一种整合多种数据类型的复合数据类型。与类不同,结构体是值类型,意味着数据被直接复制而非引用。这使其适合表示小型、固定的数据结构如点坐标。结构体默认私有成员且不可变,除非明确指定。通过`struct`关键字定义,可以包含字段、构造函数及方法。例如,定义一个表示二维点的结构体,并实现计算距离原点的方法。使用时如同普通类型,可通过实例化并调用其成员。设计时推荐保持结构体不可变以避免副作用,并注意装箱拆箱可能导致的性能影响。掌握结构体有助于构建高效的应用程序。
39 7
|
19小时前
|
程序员 C#
C# 语言类型全解
C# 语言类型全解
5 0
|
4天前
|
开发框架 .NET 编译器
C# 中的记录(record)类型和类(class)类型对比总结
C# 中的记录(record)类型和类(class)类型对比总结
|
4天前
|
传感器 开发框架 JSON
聊聊 C# dynamic 类型,并分享一个将 dynamic 类型变量转为其它类型的技巧和实例
聊聊 C# dynamic 类型,并分享一个将 dynamic 类型变量转为其它类型的技巧和实例
|
1月前
|
SQL 安全 Java
Android经典面试题之Kotlin中object关键字实现的是什么类型的单例模式?原理是什么?怎么实现双重检验锁单例模式?
Kotlin 单例模式概览 在 Kotlin 中,`object` 关键字轻松实现单例,提供线程安全的“饿汉式”单例。例如: 要延迟初始化,可使用 `companion object` 和 `lazy` 委托: 对于参数化的线程安全单例,结合 `@Volatile` 和 `synchronized`
29 6
|
2月前
|
开发框架 .NET 编译器
程序与技术分享:C#基础知识梳理系列三:C#类成员:常量、字段、属性
程序与技术分享:C#基础知识梳理系列三:C#类成员:常量、字段、属性
20 2
|
2月前
|
Java
Object当中的object类型
Object当中的object类型
|
3月前
|
安全 API C#
C#.Net筑基-类型系统②常见类型--枚举Enum
枚举(enum)是C#中的一种值类型,用于创建一组命名的整数常量。它们基于整数类型(如int、byte等),默认为int。枚举成员可指定值,未指定则从0开始自动递增。默认值为0。枚举可以与整数类型互相转换,并可通过`[Flags]`特性表示位域,支持位操作,用于多选场景。`System.Enum`类提供了如`HasFlag`、`GetName`等方法进行枚举操作。
|
3月前
|
编译器 C#
C#.Net筑基-类型系统②常见类型 --record是什么类型?
`record`在C#中是一种创建简单、只读数据结构的方式,常用于轻量级数据传输。它本质上是类(默认)或结构体的快捷形式,包含自动生成的属性、`Equals`、`ToString`、解构赋值等方法。记录类型可以继承其他record或接口,但不继承普通类。支持使用`with`语句创建副本。例如,`public record User(string Name, int Age)`会被编译为包含属性、相等比较和`ToString()`等方法的类。记录类型提供了解构赋值和自定义实现,如密封的`sealed`记录,防止子类重写。