상등 비교
값 상등 대 참조 상등
- 값 상등(Value Equality)
- 참조 상등(Referential Equality)
- 기본적으로
* 값 형식은 값 상등
* 참조 형식은 참조 상등
표준 상등 프로토콜
- == 연산자와 != 연산자
- 가상 Equals 메서드
- IEquatable<T> 인터페이스
- 교체 가능(pluggable) 프로토콜들
- IStructuralEquatable 인터페이스
== 연산자와 != 연산자
- 연산자이기 때문에 그 의미가 정적으로 구현됨
- 비교를 수행할 형식을 컴파일 시점에서 결정
- 동적 다형성(virtual 메서드)이 고려되지 않음
가상 object.Equals 메서드
- System.Object에 정의되어 있으므로 모든 형식이 이 메서드를 제공함
- 실행 시점에서, 해당 객체의 실제 형식에 근거해서 결정됨
* 값 형식: 값 상등
* 참조 형식: 참조 상등
* 구조체 형식: 구조체 상등(모든 필드에 대해서 값 상등 비교)
정적 object.Equals 메서드
- 2개의 인수를 받는 비교 매서드
- 컴파일 시점에서 미리 알 수 없는 상황에서도 널에 대해 안전한 방식으로 상등 비교 가능
- 제네릭을 이용해서 일반적 형식을 작성할 때 유용
정적 object.ReferenceEquals 메서드
- 참조 상등 비교를 강제
- 참조 형식이라고 해서 반드시 참조 상등을 따른다는 보장이 없기에 사용
* 가상 Equals 메서드 재정의
* == 연산자의 중복적재
namespace System
{
public interface IEquatable<T>
{
bool Equals(T other);
}
}
- object.Equals를 호출하면 값 형식의 피연산자들에 대해 반드시 박싱이 적용 됨. 이는 실제 비교해 비해 비싼 연산
- IEquatable<T>를 구현한 형식에 대해 Equals를 호출하면 object.Equals와 같은 결과가 나오고, 속도도 빠름.
Equals와 == 연산자가 같지 않은 경우
- NaN과 같은 경우 어떤 것과도 같지 않다고 판정 (같은 NaN이더라도)
- 반면 Equals는 반사적(Reflexive) 상등을 따름 (x.Equals(x)는 항상 true)
* 컬렉션이나 사전 자료구조는 Equals가 이런 식으로 행동한다고 가정
- 값 형식: Equals와 ==가 다른 의미의 상등일 경우가 드뭄
- 참조 형식: Equals는 값 형식을 따르도록 커스텀, ==는 참조 상등을 적용하도록 그대로 두는 편
- 굳이 동일한 방식을 쓰지 않는 이유
1. 첫 피연산자가 null이면 NullReferenceException 예외 발생, Equals의 호출이 실패. 정적 연산자는 그렇지 않음
2. == 연산자는 정적으로 결정되므로 실행속도가 빠름.
3. ==와 Equals에게 각자 다른 의미의 상등 비교를 수행하게 하는 것이 유용한 경우가 종종 있음.
상등과 커스텀 형식
- 형식을 직접 작성할 때에는 기본 행동방식 이외의 방식을 구현하는 것이 바람직할 때가 있음.
* 상등의 의미를 바꾸기 위해
* 구조체들의 상등 비교를 좀 더 빨리 수행하기 위해
상등의 의미를 변경
- ==와 Equals의 기본 행동방식이 만드는 형식에 대해 자연스럽지 않거나, 최종 사용자가 기대하는 것과는 다르다면
상등의 의미를 변경하는 것이 합리적
- DateTimeOffset: UTC DateTime만 비교하고, Offset 필드는 비교하지 않음
- float, double: 상등 비교에서 NaN 비교 논리를 지원
- System.Uri, System.String: 참조 상등 대신 값 상등 사용
구조체 상등 비교 속도 높히기
- Equals를 재정의하면 상등 비교 속도를 5배 정도 빠르게 할 수 있음
- == 연산자를 중복적재하고 IEquatable<T>를 구현하면 박싱 없는 상등 비교가 가능해져 속도를 또 다섯 배 높일 수 있음
- 구조체의 해싱 알고리즘을 개선하여 해시 테이블의 성능을 높힘
GetHashCode의 재정의
- 해시테이블(Hashtable) 자료구조
* System.Collections.Hashtable
* System.Collections.Generic.Dictionary<TKey,TValue>
- 참조 형식들, 값 형식들 모두 GetHashCode의 기본 구현이 갖추어져 있으므로 직접 재정의 할 필요는 없음
* 구조체 - 런타임이 결정하여 구현, 구조체의 모든 필드를 해싱하는 방식(느림)의 구현이 적용 될 수도 있음
* 클래스 - 내부 객체 토큰에 기초
- object.GetHashCode의 재정의 규칙
1. Equals가 true를 돌려주는 두 객체에 대해 GetHashCode도 반드시 true를 돌려주어야 함
2. 예외를 던지면 안 됨
3. 같은 객체에 대해 되풀이해서 호출 했을 때 항상 같은 값을 돌려주어야 함(객체가 변하지 않았을 때)
- Equals, GetHashCode 중 하나가 재정의 된다면 다른 한쪽도 같이 재정의 해줘야 함
Equals를 재정의 할 때 지켜야 할 규칙
- object.Equals에는 다음과 같은 공리들이 성립
* 객체와 null은 상등이 되지 않음 (널 가능 형식이 아닌 한)
* 반사적(Reflexiv) : 객채는 자신과 상등
* 가환적(Commutative) : 만일 a.Equals(b)이면 b.Equals(a)도 true
* 추이적(transitive, 전이적) : 만일 a.Equals(b)이고, b.Equals(c)이면 a.Equals(c)도 true
* 상등 연산은 되풀이 될 수 있고, 안정적 : 예외를 던지지 않음
==와 !=의 중복적재
- 구조체
* 구조체에서는 거의 항상 이 연산자들을 중복 적재
- 클래스
* 그대로 둠. 즉, 기본 형식인 참조 상등을 적용
: 커스텀 형식들, 특히 가변이(mutable) 형식들에서 주로 사용
* Equals에 맞게 중복적재
: 소비자가 절대로 참조 상등을 원하지 않을 형식들에서 주로 사용(string, System.Uri, 일부 Struct)
IEquatable<T>의 구현
- Equals를 재정의 할 때 IEquatable<T>도 같이 구현하는것이 좋음
GetHashCode의 구현
- 죠슈아 블로크(Joshua Bloch)가 제안한 패턴, 무난한 성능 기대 가능
int hash = 17; // 임의의 소수
int prime = 31; // 또 다른 소수
hash = hash * prime + field1.GetHashCode();
hash = hash * prime + field2.GetHashCode();
...
return hash;
using System;
namespace Practice
{
class Program
{
static void Main(string[] args)
{
object x = 5;
object y = 5;
// 가상 Object.Equals
Console.WriteLine(x.Equals(y));
// 정적 object.Equals
Console.WriteLine(Equals(x, y));
// 참조 상등 비교 강제
Console.WriteLine(ReferenceEquals(x, y));
Console.WriteLine();
// NaN의 비교
double z = double.NaN;
Console.WriteLine(z == z);
Console.WriteLine(z.Equals(z));
Console.WriteLine(ReferenceEquals(z, z));
Console.WriteLine();
// 예제: Area 구조체 사용
Area a1 = new Area(5, 10);
Area a2 = new Area(10, 5);
Console.WriteLine(a1.Equals(a2));
Console.WriteLine(a1 == a2);
}
// 예제: Area 구조체 구현
public struct Area : IEquatable<Area>
{
public readonly int[] measure;
public Area (int m1, int m2)
{
measure = new int[2];
measure[0] = Math.Min(m1, m2);
measure[1] = Math.Max(m1, m2);
}
public override bool Equals(object other)
{
// other(object)가 Area 형식으로 변환이 불가능하다면
if(!(other is Area))
{
return false;
}
else
{
// 인터페이스로 구현한 메소드 호출
return Equals((Area) other);
}
}
// 인터페이스 구현
public bool Equals(Area other)
=> measure[0] == other.measure[0] && measure[1] == other.measure[1];
// 31은 임의로 선택한 소수
public override int GetHashCode()
=> measure[1] * 31 + measure[0];
// == 연산자 오버로딩
public static bool operator ==(Area a1, Area a2) => a1.Equals(a2);
// != 연산자 오버로딩
public static bool operator !=(Area a1, Area a2) => !a1.Equals(a2);
}
}
}
'C#' 카테고리의 다른 글
[C#] Process (1) | 2023.08.29 |
---|---|
[C#] Environment (0) | 2023.08.28 |
[C#] Console (1) | 2023.08.27 |
[C#] 순서 비교 (IComparable<T>, IComparable) (1) | 2023.08.26 |
[C#] Guid (0) | 2023.08.24 |
[C#] Math (1) | 2023.08.23 |
[C#] Tuple (1) | 2023.08.22 |
[C#] 열거형과 System.Enum (1) | 2023.08.21 |