코딩 연습 문제는 C#에서 동등성이 갖는 여러 속성을 보여줘요.
Object.Equals()==와 !=로 동등성을 검사해요. 이는 이런 타입에서도 사용할 수 있는 Equals() 메서드를 쓰는 것보다 더 관용적인 방식으로 여겨져요. Java 프로그래머는 문자열을 다룰 때, C#에서는 ==가 값으로 비교하지만 Java에서는 참조로 비교한다는 점에 주의해야 해요.object에서 상속받은 Equals() 메서드로 비교해요. 동등성 검사의 목적이 두 객체가 정확히 같은 인스턴스인지 확인하는 것이라면 object의 구현을 그대로 쓰면 충분해요. 그렇지 않다면 object.Equals()를 재정의해야 해요.Equals()를 재정의해야 하죠.Equals()에는 단순 타입 멤버에 대한 == 비교와 참조 타입 멤버에 대한 재귀적 Equals() 호출이 들어가요.class StatusBar
{
private readonly int width = 200, height = 20;
public override bool Equals(object other)
{
// ... null and type checks and performance optimisations
return width == (other as StatusBar).width && height == (other as StatusBar).height;
}
}
class Window
{
private readonly string title = "Main";
private readonly StatusBar statusBar = new StatusBar();
public override bool Equals(object other)
{
// ... null and type checks and performance optimisations
return title == (other as Window).title && statusBar.Equals((other as Window).statusBar);
}
}
object.ReferenceEquals()는 두 객체가 같은 인스턴스인지 판별할 때 사용해요. 이는 코드를 명확하게 해 주고, Equals()와 ==를 재정의하거나 오버로드한 경우에는 꼭 필요해요.var winA = new Window(); // above code shows that all windows are equal
var winB = new Window();
ReferenceEquals(winA, winB);
// => false
var winC = winA;
ReferenceEquals(winA, winC);
// => true
public override bool Equals(object obj) 외에도, IDE는 보통 상속이 관련될 때 사용할 protected bool Equals(FacialFeatures other) 오버로드를 생성해요. 파생 클래스는 기반 클래스의 Equals()를 호출한 뒤 자신만의 검사를 추가할 수 있어요.== 연산자와 클래스의 Equals() 메서드를 모두 오버로드했거나(operator-overloading 연습 문제를 참고하세요), 참조가 같은지만 확인하면 되는 경우가 아니라면 ==는 쓰지 않는 게 좋아요.struct의 동등성 검사는 structs 연습 문제에서 다뤄요.tuple의 동등성은 tuples 연습 문제에서 다뤄요.protected bool Equals(T other)
{
return field1 == other.field1 && field2.Equals(other.field2);
}
public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != this.GetType()) return false;
return Equals((T) obj);
}
SequenceEquals()를 제공하지만, LINQ가 없다면 두 컬렉션을 순회하며 항목을 하나씩 비교하는 수밖에 없어요.==와 !=를 어떻게 사용하는지는 operator-overloading 연습 문제를 참고하세요.Object.GetHashCode()object.GetHashCode()는 32비트 정수 형태의 해시 코드를 반환해요. 해시 코드는 Dictionary<T>나 HashSet<T> 같은 딕셔너리와 집합 클래스가 객체를 효율적으로 저장하고 조회하는 데 사용돼요. 딕셔너리의 경우 해싱은 키와 관련이 있어요.Equals()를 재정의하면 GetHashCode()도 함께 재정의해야 한다는 기대가 있어요. Equals()와 GetHashCode() 사이에는 관계가 있는데, 이는 딕셔너리와 해시 집합 클래스, 그리고 해시 코드를 사용하는 다른 클래스들이 올바르게 동작하려면 반드시 지켜져야 해요. 나중에 해시 코드 기반 컬렉션을 추가할지도 모르는 유지보수자에게 함정을 남기지 않도록 이 메서드를 구현해야 해요.Equal()이 true를 반환한다면) 두 객체의 GetHashCode()는 같은 값을 반환해야 해요. 하지만 그 반대 방향은 성립하지 않아요. 대칭적이지 않죠. 해시 코드를 기준으로 먼저 "버킷"으로 간 다음, 동등성 검사를 통해 특정 항목을 골라내는 조회 함수를 떠올려 보세요.HashCode.Combine()을 호출하는 거예요. Combine()에 전달하는 정보가 많을수록 해시 구현의 성능이 더 좋아질 가능성이 크다는 점을 기억하세요.public class Assessment
{
private int rating;
private Person boss;
public override int GetHashCode()
{
return HashCode.Combine(rating, boss);
}
}
성능을 조금 향상하려면, 특히 객체가 컬렉션에 속하는 경우 public bool Equals(T other) 오버로드 멤버를 추가할 수 있어요.
이렇게 하면 참조 타입에서는 타입 검사를 어느 정도 줄일 수 있고, 값 타입에서는 public override bool Equals(object other)의 인자로 전달되기 위해 객체로 변환(박싱)될 필요가 없어 박싱 단계를 하나 줄일 수 있어요.
클래스에 IEquatable<T> 인터페이스를 추가하면 이 오버로드를 반드시 구현해야 해요. 코드에 IEquatable<T> 타입의 객체를 받는 루틴(아마도 구현 클래스와 무관하게 동등성과 관련된 흥미로운 작업을 하는 루틴)이 없다면, 인터페이스를 포함시킬 만한 다른 설득력 있는 이유는 사실상 없어요.
IEqualityComparer<T>두 가지 서로 다른 방식으로 고유하게 식별할 수 있는 클래스가 있다고 해봐요. 예를 들어 SSID와 고유한 이메일 주소를 가진 Person 클래스라면, .NET은 두 개의 서로 다른 컬렉션이 각기 다른 해시 코드와 동등성 검사를 사용하도록 하는 방법을 제공해요. 각 컬렉션은 자체 Equals()와 GetHashCode() 메서드를 가진 서로 다른 IEqualityComparer<T> 구현을 가질 수 있어요. SSID를 키로 하는 딕셔너리와 이메일 주소를 키로 하는 딕셔너리를 각각 만들 수 있죠.
IEqualityComparer<T>를 사용하더라도, 컬렉션 클래스 밖에서 생길 수 있는 문제를 피하려면 보통 항목 클래스에서 Equals()와 GetHashCode()를 여전히 재정의해요.
IEqualityComparer<T>를 사용할 때 고려할 점은, private 메서드 등은 동등성 검사에 사용할 수 없다는 거예요.
해시 컬렉션이 하나뿐이라면 IEqualityComparer<T>를 피하고 동등성과 해시 코드를 객체 자체에 캡슐화하는 편이 더 나을 수 있어요. 객체와 해시 컬렉션 사이의 핵심적인 의존 관계를 컴파일러가 강제할 수 없다는 것 자체가 이상적이지는 않지만, 해싱과 비교 로직이 다른 곳에 있으면 유지보수자가 컬렉션에 미칠 영향을 고려하지 않고 클래스 멤버를 변경하기가 훨씬 쉬워져요.
부주의한 코더를 곤란하게 만들 수 있는 기본형 중 하나는 부동소수점 값의 동등성을 검사하는 거예요. 이는 floating-point-numbers 개념의 about.md 문서에서 다뤄요.
이 글은 동등성과 상속과 관련해 내려야 하는 몇 가지 결정을 보여줘요. 이는 클래스와 파생 클래스의 인스턴스를 함께 다루면서 둘 사이의 동등성을 검사해야 할 때만 해당하는 이야기예요.