"Set"에 Get 메소드가 있어야합니까?


22

이 C # 클래스를 사용합시다 (Java에서 거의 동일 함)

public class MyClass {
   public string A {get; set;}
   public string B {get; set;}

   public override bool Equals(object obj) {
        var item = obj as MyClass;

        if (item == null || this.A == null || item.A == null)
        {
            return false;
        }
        return this.A.equals(item.A);
   }

   public override int GetHashCode() {
        return A != null ? A.GetHashCode() : 0;
   }
}

보시다시피, 두 인스턴스의 동등성은 에만 MyClass의존 A합니다. 따라서 동일하지만 B속성 에 서로 다른 정보를 보유하는 두 개의 인스턴스가있을 수 있습니다 .

많은 언어 (물론 C # 및 Java 포함)의 표준 컬렉션 라이브러리에는 Set( HashSetC #에) 컬렉션이 있습니다.이 컬렉션은 각 동일한 인스턴스 집합에서 최대 하나의 항목을 보유 할 수 있습니다.

항목을 추가하고, 제거하고, 세트에 항목이 포함되어 있는지 확인할 수 있습니다. 그러나 세트에서 특정 품목을 얻는 것이 왜 불가능합니까?

HashSet<MyClass> mset = new HashSet<MyClass>();
mset.Add(new MyClass {A = "Hello", B = "Bye"});

//I can do this
if (mset.Contains(new MyClass {A = "Hello", B = "See you"})) {
    //something
}

//But I cannot do this, because Get does not exist!!!
MyClass item = mset.Get(new MyClass {A = "Hello", B = "See you"});
Console.WriteLine(item.B); //should print Bye

내 항목을 검색하는 유일한 방법은 전체 컬렉션을 반복하고 모든 항목이 동일한 지 확인하는 것입니다. 그러나, O(n)대신에 시간 이 걸립니다 O(1)!

지금까지 세트에서 얻을 수있는 언어를 찾지 못했습니다. 내가 아는 모든 "공통"언어 (Java, C #, Python, Scala, Haskell ...)는 같은 방식으로 디자인 된 것 같습니다. 항목을 추가 할 수는 있지만 검색 할 수는 없습니다. 이 모든 언어가 쉽고 분명한 기능을 지원하지 않는 이유가 있습니까? 그들은 모두 잘못 될 수 없습니다. 그것을 지원하는 언어가 있습니까? 어쩌면 세트에서 특정 아이템을 가져 오는 것이 잘못되었을 수 있지만 왜 그런가요?


SO 관련 몇 가지 질문이 있습니다.

/programming/7283338/getting-an-element-from-a-set

/programming/7760364/how-to-retrieve-actual-item-from-hashsett


12
C ++ std::set은 객체 검색을 지원하므로 모든 "공통"언어가 설명하는 것과는 다릅니다.
Monica Monica

17
당신이 그 (코드) 주장하는 경우 동일한 값과 다른 B를 가지고 "MyClass의 두 인스턴스의 평등이에 따라 달라집니다 만"다음 다른 인스턴스가 효율적 이다 당신 자신을 정의하기 때문에, "그 특정 인스턴스"그들이있는 거 같고 B의 차이는 중요하지 않습니다. 컨테이너는 동일하므로 다른 인스턴스를 반환하도록 "허용"됩니다.
Peteris

7
사실 : Java에서는 많은 Set<E>구현이 Map<E,Boolean>내부에 있습니다.
corsiKa

10
사람 A에게 말하기 : "안녕하세요, 사람 A를 여기로 데려 오세요"
Brad Thomas

7
이것은 a == b경우에 따라 반사성을 깨뜨립니다 ( 항상 참) this.A == null. if (item == null || this.A == null || item.A == null)테스트 가능성이 인위적으로 "고품질"코드를 생성하기 위해, 많은을 "과장"를 확인합니다. 코드 검토에서 이러한 종류의 "과도하게 검사"하고 항상 올바르게 수정하는 것을 볼 수 있습니다.
usr

답변:


66

여기서 문제 HashSetGet메서드 가 부족 하지 않다는 것입니다. 코드의 관점에서 HashSet유형 의 관점에서 말이 안됩니다 .

Get방법은 .NET 프레임 워크 사람들이 "eh? 이미 그 값을 가지고 있습니다"라고 대답 할 수있는 "이 값을 가져 오십시오"라는 효과적인 방법입니다 <confused face />.

항목을 저장 한 다음 약간 다른 값과 일치하여 항목을 검색하려면 다음 Dictionary<String, MyClass>과 같이하십시오.

var mset = new Dictionary<String, MyClass>();
mset.Add("Hello", new MyClass {A = "Hello", B = "Bye"});

var item = mset["Hello"];
Console.WriteLine(item.B); // will print Bye

캡슐화 된 클래스에서 동등 정보가 누출됩니다. 에 관련된 속성 집합 Equals을 변경하려면 외부에서 코드를 변경해야합니다 MyClass...

그렇습니다. 그러나 그것은 MyClass가장 놀랍게도 (POLA)의 원리로 실행 되기 때문 입니다. 동일한 기능이 캡슐화되면 다음 코드가 유효하다고 가정하는 것이 합리적입니다.

HashSet<MyClass> mset = new HashSet<MyClass>();
mset.Add(new MyClass {A = "Hello", B = "Bye"});

if (mset.Contains(new MyClass {A = "Hello", B = "See you"})) 
{
    // this code is unreachable.
}

이를 방지하려면 MyClass홀수 형태의 평등에 대해 명확하게 문서화해야합니다. 그렇게 한 후에는 더 이상 캡슐화되지 않으며 평등이 작동하는 방식을 변경하면 개방 / 폐쇄 원칙을 깨뜨릴 수 있습니다. Ergo, 변경해서는 안되므로이 Dictionary<String, MyClass>이상한 요구 사항에 대한 좋은 솔루션입니다.


2
@vojta,이 경우 사용 Dictionary<MyClass, MyClass>하는 키를 기준으로 값을 가져옵니다 MyClass.Equals.
David Arno

8
Dictionary<MyClass, MyClass>적절한을 사용하여 공급품을 사용하고 해당 인스턴스에 대해이 관계에 대해 알아야 IEqualityComparer<MyClass>하는 MyClass이유는 무엇 MyClass입니까?
Caleth

16
@vojta와 그에 대한 주석 : " meh. 같지 않은 객체가"같아 "도록 equals의 구현을 재정의하는 것은 여기서 문제입니다."이 객체와 동일한 객체를 가져 오십시오 "라는 메소드를 요청한 다음 동일하지 않은 오브젝트가 리턴 될 것으로 예상하면 유지 보수 문제점을 일으키는 것은 미치며 쉬운 것 같습니다 . 그것은 종종 SO의 문제입니다. 깨진 코드에 대한 빠른 수정을 원한다는 암시를 통해 생각하지 않은 사람들은 심각하게 잘못된 답변을지지합니다.
David Arno

6
@DavidArno : 평등과 정체성을 구별하는 언어를 사용하는 한 피할 수없는 것입니다. ;-) 동일하지만 동일하지 않은 객체를 정규화하려면 "나에게 동일하게 해주지 않는 방법이 필요합니다. 이 개체에 대한 객체 "라고 표시하지만"이 객체와 동일한 정식 객체를 가져 오십시오 ". 이러한 언어로 된 HashSet.Get이 반드시 "동일한 객체를 얻습니다"라는 의미는 이미 심각하게 잘못되었다고 생각하는 사람이라면
Steve Jessop

4
이 답변에는와 같은 많은 담요 설명이 있습니다 ...reasonable to assume.... 이 모든 것이 99 %의 경우에 해당 될 수 있지만 세트에서 항목을 검색하는 기능은 여전히 ​​유용 할 수 있습니다. 실제 코드는 항상 POLA 등의 원칙을 준수 할 수 없습니다. 예를 들어, 대소 문자를 구분하지 않고 문자열을 중복 제거하는 경우 "마스터"항목을 가져올 수 있습니다. Dictionary<string, string>해결 방법이지만 성능이 저하됩니다.
usr

24

세트에 "있는"항목이 이미 있습니다. 키로 전달했습니다.

"하지만 Add with라고 한 인스턴스가 아닙니다"-네,하지만 당신은 그것들이 동일하다고 주장했습니다.

A Set는 또한 Map| Dictionary값 유형으로 void를 사용하여 쓸모없는 메소드는 정의되지 않지만 중요하지 않습니다.

당신이 찾고있는 데이터 구조는 어떻게 든 MyClasses에서 As를 얻는 Dictionary<X, MyClass>X입니다.

C # Dictionary 유형은 키에 IEqualityComparer를 제공 할 수 있기 때문에 이와 관련하여 좋습니다.

주어진 예를 들어, 나는 다음을 가질 것입니다 :

public class MyClass {
   public string A {get; set;}
   public string B {get; set;}
}

public class MyClassEquivalentAs : IEqualityComparer<MyClass>{
   public override bool Equals(MyClass left, MyClass right) {
        if (Object.ReferenceEquals(left, null) && Object.ReferenceEquals(right, null))
        {
            return true;
        }
        else if (Object.ReferenceEquals(left, null) || Object.ReferenceEquals(right, null))
        {
            return false;
        }
        return left.A == right.A;
   }

   public override int GetHashCode(MyClass obj) {
        return obj?.A != null ? obj.A.GetHashCode() : 0;
   }
}

따라서 다음과 같이 사용됩니다.

var mset = new Dictionary<MyClass, MyClass>(new MyClassEquivalentAs());
var bye = new MyClass {A = "Hello", B = "Bye"};
var seeyou = new MyClass {A = "Hello", B = "See you"};
mset.Add(bye);

if (mset.Contains(seeyou)) {
    //something
}

MyClass item = mset[seeyou];
Console.WriteLine(item.B); // prints Bye

키와 일치하는 객체를 가진 코드가 키로 사용 된 객체에 대한 참조로 대체하는 것이 유리한 경우가 많이 있습니다. 예를 들어, 많은 문자열이 해시 컬렉션의 문자열과 일치하는 것으로 알려진 경우 모든 문자열에 대한 참조를 컬렉션의 문자열에 대한 참조로 바꾸면 성능이 향상 될 수 있습니다.
supercat

@supercat 오늘은 Dictionary<String, String>.
MikeFHay

@ MikeFHay : 예, 그러나 각 문자열 참조를 두 번 저장 해야하는 것은 약간 우아하지 않습니다.
supercat

2
@supercat 만약 당신이 동일한 문자열 을 의미한다면 , 그것은 단지 문자열 인턴입니다. 내장 된 물건을 사용하십시오. 일종의 "정규"표현 ( 단순한 사례 변경 기술 등으로는 달성 할 수없는 표현)을 의미하는 경우 기본적으로 색인이 필요하다고 생각되는 것 같습니다 (데이터베이스에서 용어를 사용한다는 의미). 각 "비정규 양식"을 표준 양식에 맵핑하는 키로 저장하는 데 문제점이 없습니다. ( "정규"형식이 문자열이 아닌 경우에도 똑같이 적용됩니다.) 이것이 당신이 말하는 것이 아닌 경우, 당신은 완전히 나를 잃어버린 것입니다.
jpmc26

1
관습 Comparer이며 Dictionary<MyClass, MyClass>실용적인 솔루션입니다. Java에서는 TreeSet또는 TreeMapcustom을 통해 동일하게 수행 할 수 있습니다 Comparator.
Markus Kull

19

문제는 두 가지 모순 된 평등 개념이 있다는 것입니다.

  • 모든 필드가 동일한 실제 평등
  • 멤버십 평등을 설정합니다. 여기서 A 만 같습니다

세트에서 실제 동등 관계를 사용하는 경우 세트에서 특정 항목을 검색하는 문제가 발생하지 않습니다. 오브젝트가 세트에 있는지 여부를 확인하기 위해 해당 오브젝트가 이미 있습니다. 따라서 올바른 동등 관계를 사용한다고 가정하면 세트에서 특정 인스턴스를 검색 할 필요는 없습니다.

또한 집합 이 or 관계에 의해 정의 된 추상 데이터 유형 이라고 주장 할 수도 있습니다 (“특성 기능”). 다른 작업을 원한다면 실제로 세트를 찾고 있지 않습니다.S contains xx is-element-of S

꽤 자주 발생하지만 설정되지 않은 것은 모든 객체를 별개의 등가 클래스 로 그룹화한다는 것 입니다. 이러한 각 클래스 또는 서브 세트의 오브젝트는 동일하지 않고 동일합니다. 우리는 해당 하위 집합의 멤버를 통해 각 동등성 클래스를 나타낼 수 있으며, 해당하는 요소를 검색하는 것이 바람직합니다. 이는 동등성 클래스에서 대표 요소 로의 맵핑 입니다.

C #에서 사전은 명시 적 평등 관계를 사용할 수 있다고 생각합니다. 그렇지 않으면, 빠른 랩퍼 클래스를 작성하여 이러한 관계를 구현할 수 있습니다. 의사 코드 :

// The type you actually want to store
class MyClass { ... }

// A equivalence class of MyClass objects,
// with regards to a particular equivalence relation.
// This relation is implemented in EquivalenceClass.Equals()
class EquivalenceClass {
  public MyClass instance { get; }
  public override bool Equals(object o) { ... } // compare instance.A
  public override int GetHashCode() { ... } // hash instance.A
  public static EquivalenceClass of(MyClass o) { return new EquivalenceClass { instance = o }; }
}

// The set-like object mapping equivalence classes
// to a particular representing element.
class EquivalenceHashSet {
  private Dictionary<EquivalenceClass, MyClass> dict = ...;
  public void Add(MyClass o) { dict.Add(EquivalenceClass.of(o), o)}
  public bool Contains(MyClass o) { return dict.Contains(EquivalenceClass.of(o)); }
  public MyClass Get(MyClass o) { return dict.Get(EquivalenceClass.of(o)); }
}

"세트에서 특정 인스턴스 검색" "인스턴스"를 "멤버"로 변경 한 경우 더 직접적인 의미를 전달한다고 생각합니다. 사소한 제안 일뿐입니다. =) +1
jpmc26

7

그러나 세트에서 특정 품목을 얻는 것이 왜 불가능합니까?

그게 세트가 아니기 때문입니다.

예제를 다시 설명하겠습니다.

"MyClass 객체를 저장하려는 HashSet이 있고 객체의 속성 A와 동일한 속성 A를 사용하여 가져올 수 있기를 원합니다."

"HashSet"을 "Collection"으로, "objects"를 "Values"로, "Property A"를 "Key"로 바꾸면 문장이 다음과 같이됩니다.

"저는 MyClass 값을 저장하려는 Collection을 가지고 있으며 객체의 Key와 동일한 Key를 사용하여이를 얻을 수 있기를 원합니다."

설명되는 것은 사전입니다. 실제 질문은 "HashSet을 사전으로 취급 할 수없는 이유는 무엇입니까?"입니다.

대답은 그것들이 같은 것에 사용되지 않는다는 것입니다. 세트를 사용하는 이유는 개별 컨텐츠의 고유성을 보장하기위한 것입니다. 그렇지 않으면 List 또는 배열을 사용할 수 있습니다. 질문에 설명 된 동작은 사전의 목적입니다. 모든 언어 디자이너는 망치지 않았습니다. 객체가 있고 세트에 있으면 동등하므로 get 메소드를 제공하지 않습니다. 즉, 동등한 오브젝트를 "얻는"것입니다. 언어가 다른 데이터 구조를 제공 할 때 HashSet이 동일하다고 정의한 동일하지 않은 객체를 "가져올"수있는 방식으로 HashSet을 구현해야한다고 주장하는 것은 스타터가 아닙니다.

OOP 및 평등 의견 / 답변에 대한 메모. 매핑 키가 사전에 저장된 값의 속성 / 구성원이되도록해도됩니다. 예를 들어, Guid를 키로 사용하고 equals 메소드에 사용되는 특성도 완벽하게 합리적입니다. 합리적이지 않은 것은 나머지 속성에 대해 다른 값을 갖는 것입니다. 그 방향으로 가고 있다면 수업 구조를 다시 생각해야 할 것입니다.


6

equals를 재정의하면 해시 코드를 재정의하는 것이 좋습니다. 이 작업을 수행하자마자 "인스턴스"는 다시 내부 상태를 변경하지 않아야합니다.

equals를 재정의하지 않으면 hashcode VM 객체 ID를 사용하여 동등성을 결정합니다. 이 개체를 세트에 넣으면 다시 찾을 수 있습니다.

동등성을 결정하는 데 사용되는 객체의 값을 변경하면 해시 기반 구조에서이 객체를 추적 할 수 없게됩니다.

따라서 A의 세터는 위험합니다.

이제 평등에 참여하지 않는 B가 없습니다. 여기서 문제는 기술적으로 의미가 없습니다. 기술적으로 B를 바꾸는 것은 평등의 사실에 중립이기 때문입니다. 의미 상 B는 "version"플래그와 같아야합니다.

요점은 :

A와 같지만 B가 아닌 두 개의 오브젝트가있는 경우이 오브젝트 중 하나가 다른 오브젝트보다 최신이라고 가정합니다. B에 버전 정보가없는 경우이 가정은 알고리즘에서 숨겨져 있으며이 객체를 세트에서 "덮어 쓰기 / 업데이트"하기로 결정한 경우. 이것이 발생하는이 소스 코드 위치는 분명하지 않을 수 있으므로 개발자는 B의 X와 다른 객체 X와 객체 Y 사이의 관계를 식별하기가 어렵습니다.

B에 버전 정보가있는 경우 이전에 코드에서 암시 적으로 파생 할 수 있다는 가정을 표시합니다. 이제 객체 Y가 최신 버전의 X라는 것을 알 수 있습니다.

자신에 대해 생각하십시오 : 당신의 정체성은 평생 동안 유지 될 수 있으며, 일부 속성은 변할 수 있습니다 (예 : 머리카락 색깔 ;-)). 갈색 머리가있는 사진과 회색 머리가있는 사진이 갈색 인 사진이 더 젊을 수 있다고 가정 할 수 있습니다. 그러나 당신은 당신의 머리를 착색했을까요? 문제는 당신이 당신이 당신의 머리 색깔을 알고 있음을 알 수 있습니다. 다른 사람이 있습니까? 이것을 유효한 문맥에 넣으려면 속성 연령 (버전)을 소개해야합니다. 그렇다면 당신은 의미 상 명백하고 모호하지 않습니다.

"오래된 객체에 대해 오래된 것으로 바꾸는"숨겨진 작업을 피하려면 Set에 get-Method가 없어야합니다. 이와 같은 동작을 원하면 이전 객체를 제거하고 새 객체를 추가하여 명시 적으로 만들어야합니다.

BTW : 얻고 자하는 물체와 같은 물체를 통과하면 무슨 의미입니까? 말이되지 않습니다. 기술적으로 아무도 당신을 방해하지는 않지만 의미를 깨끗하게 유지하고 이것을하지 마십시오.


7
"오버라이드하면 해시 코드를 오버라이드하는 것이 좋습니다.이 작업을 마치면"인스턴스 "는 다시 내부 상태를 변경하지 않아야합니다." 그 진술은 +100의 가치가 있습니다.
David Arno

가변 상태에 따라 평등 및 해시 코드의 위험을 지적하기위한 +1
Hulk

3

특히 자바에서는 어쨌든 값을 무시하고 어쨌든 HashSet사용하여 구현되었습니다 HashMap. 따라서 초기 디자인은에 get 메소드를 제공 할 때 어떤 이점도 기대하지 않았습니다 HashSet. 동일한 다양한 객체 중에서 표준 값을 저장하고 검색하려면 HashMap자신을 사용 하십시오.

나는 그러한 구현 세부 사항을 최신 상태로 유지하지 않았 으므로이 추론이 여전히 C # 등은 물론 Java로 완전히 적용되는지 말할 수는 없습니다. HashSetHashMap 어떤 경우에도 보다 적은 메모리를 사용하도록 다시 구현 된 경우에도 Set인터페이스에 새로운 메소드를 추가하기위한 주요 변경 사항 입니다. 따라서 모든 사람이 가치가 있다고 생각하지는 않지만 상당한 이익을 얻습니다.


글쎄, 자바에서는 이것을 비 구현적인 방법으로 구현할 있다 default. 그다지 유용한 변화는 아닙니다.
Hulk

@Hulk : 틀렸을 수도 있지만, 질문자가 "아이템을 검색하는 유일한 방법은 전체 컬렉션을 반복하고 모든 아이템이 동일한 지 확인하는 것"이기 때문에 기본 구현은 상당히 비효율적이라고 생각합니다. 따라서 좋은 점은 이전 버전과 호환되는 방식으로 수행 할 수 있지만 O(n)해시 함수가 좋은 분포를 제공하더라도 결과 get 함수가 비교 에서만 실행되도록 보장하는 gotcha를 추가하는 것입니다. 그런 다음 구현을 Set포함하여 인터페이스의 기본 구현을 재정의하면 HashSet더 나은 보증을 제공 할 수 있습니다.
Steve Jessop

동의-좋은 생각이 아닌 것 같습니다. 이 비록 행동의 이런 종류의 우선 순위를 가지고 것 - List.get (INT 지수) 또는 - 최근에 추가 된 기본 구현을 선택하는 List.sort을 . 인터페이스는 최대한의 복잡성을 보장하지만 일부 구현은 다른 구현보다 훨씬 더 우수 할 수 있습니다.
헐크

2

원하는 속성을 가진 주요 언어가 있습니다.

C ++에서는 std::set순서가 설정됩니다. .find순서 연산자를 기반으로 요소를 찾는 방법 이 있습니다.< 또는 이진 bool(T,T)귀하가 제공하는 기능을. find를 사용하여 원하는 get 작업을 구현할 수 있습니다.

실제로 bool(T,T)제공 하는 함수에 특정 플래그가 있으면 ( is_transparent) 가있는 경우 함수에 과부하가 다른 유형의 . 즉, "더미"데이터를 두 번째 필드에 고정 할 필요가 없으며, 사용하는 순서 지정 조작이 조회 유형과 세트 포함 유형 사이에서 주문할 수 있는지 확인하십시오.

이를 통해 효율성을 높일 수 있습니다.

std::set< std::string, my_string_compare > strings;
strings.find( 7 );

어디에 my_string_compare 정수를 문자열로 변환하지 않고 정수와 문자열을 주문하는 방법을 이해합니다 (잠재적 비용으로).

내용 unordered_set(C ++의 해시 세트), (아직) 상응 투명 플래그 없다. 당신은 통과해야합니다T 으로 unordered_set<T>.find하는 방법. 추가 할 수는 있지만 ==주문이 필요한 주문 세트와 달리 해시는 해시가 필요합니다.

일반적인 패턴은 컨테이너가 조회를 수행 한 다음 컨테이너 내의 해당 요소에 대한 "반복자"를 제공한다는 것입니다. 어느 시점에서 세트 내의 요소를 가져 오거나 삭제할 수 있습니다.

간단히 말해, 모든 언어의 표준 컨테이너에 설명 된 결함이있는 것은 아닙니다. C ++ 표준 라이브러리의 반복자 기반 컨테이너는 설명하지 않았으며 적어도 일부 컨테이너는 설명 된 다른 언어 이전에 존재했으며 더 효율적 으로 얻을 수있는 기능 하는 방법보다 이 추가되었습니다. 디자인에 문제가 있거나 그 작업을 원하는 것은 없습니다. 사용중인 세트의 디자이너는 해당 인터페이스를 제공하지 않았습니다.

C ++ 표준 컨테이너는 동등한 수동 롤링 C 코드의 하위 수준 작업을 깔끔하게 래핑하도록 설계되었으며, 어셈블리에서 효율적으로 작성하는 방법과 일치하도록 설계되었습니다. 반복자는 C 스타일 포인터의 추상화입니다. 언급 한 언어는 모두 포인터로 개념에서 멀어 지므로 반복자 추상화를 사용하지 않았습니다.

C ++에이 결함이 없다는 사실은 디자인의 우연 일 수 있습니다. 반복자 중심의 경로는 연관 컨테이너의 항목과 상호 작용하기 위해 먼저 요소에 대한 반복자를 얻은 다음 해당 반복자를 사용하여 컨테이너의 항목에 대해 이야기합니다.

비용은 추적해야하는 반복 무효화 규칙이 있으며 일부 작업에는 한 단계 대신 2 단계가 필요하므로 클라이언트 코드가 더 시끄 럽습니다. 장점은 강력한 추상화가 API 디자이너가 원래 생각했던 것보다 더 진보 된 사용을 허용한다는 것입니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.