무의미한 기본값을 가진 구조체


12

내 시스템에서 나는 자주 공항 코드 (작동 "YYZ", "LAX", "SFO", 등), 그들은 (대문자로 표시 3 문자) 정확한 항상 동일한 형식. 이 시스템은 일반적으로 API 요청 당 25-50 개의 이러한 (다른) 코드를 처리하며 총 1,000 개가 넘는 할당을 처리하며 응용 프로그램의 여러 계층을 통해 전달되며 평등을 위해 자주 비교됩니다.

우리는 문자열을 전달하는 것으로 시작했습니다. 약간 잘 작동했지만 3 자리 코드가 예상되는 어딘가에 잘못된 코드를 전달하여 많은 프로그래밍 실수를 신속하게 발견했습니다. 또한 대소 문자를 구분하지 않고 비교 해야하는 문제가 발생하여 버그가 발생하지 않았습니다.

이로부터 문자열 전달을 중단 Airport하고 공항 코드를 가져와 유효성을 검사하는 단일 생성자가있는 클래스를 만들기로 결정했습니다 .

public sealed class Airport
{
    public Airport(string code)
    {
        if (code == null)
        {
            throw new ArgumentNullException(nameof(code));
        }

        if (code.Length != 3 || !char.IsLetter(code[0]) 
        || !char.IsLetter(code[1]) || !char.IsLetter(code[2]))
        {
            throw new ArgumentException(
                "Must be a 3 letter airport code.", 
                nameof(code));
        }

        Code = code.ToUpperInvariant();
    }

    public string Code { get; }

    public override string ToString()
    {
        return Code;
    }

    private bool Equals(Airport other)
    {
        return string.Equals(Code, other.Code);
    }

    public override bool Equals(object obj)
    {
        return obj is Airport airport && Equals(airport);
    }

    public override int GetHashCode()
    {
        return Code?.GetHashCode() ?? 0;
    }

    public static bool operator ==(Airport left, Airport right)
    {
        return Equals(left, right);
    }

    public static bool operator !=(Airport left, Airport right)
    {
        return !Equals(left, right);
    }
}

이를 통해 코드를 이해하기 쉽게 만들었고 평등 검사, 사전 / 집합 사용법을 단순화했습니다. 이제 메소드 Airport가 예상 한대로 작동 하는 인스턴스를 허용하면 메소드 검사를 널 참조 검사로 단순화했습니다.

그러나 내가 알아 차린 것은 가비지 수집이 훨씬 더 자주 실행되어 수집되는 많은 인스턴스를 추적했습니다 Airport.

이것에 대한 나의 해결책은를로 변환하는 것이 었 class습니다 struct. 대부분 그냥 키워드 변경은 제외했다 GetHashCodeToString:

public override string ToString()
{
    return Code ?? string.Empty;
}

public override int GetHashCode()
{
    return Code?.GetHashCode() ?? 0;
}

default(Airport)사용 된 경우를 처리합니다 .

내 질문 :

  1. Airport클래스를 만들 거나 일반적인 솔루션을 만들었습니까? 아니면 유형을 만들어서 잘못된 문제를 해결하거나 잘못된 방법으로 문제를 해결하고 있습니까? 좋은 해결책이 아니라면 더 좋은 해결책은 무엇입니까?

  2. 응용 프로그램 default(Airport)이 사용되는 인스턴스를 어떻게 처리해야 합니까? 유형은 default(Airport)내 응용 프로그램에 무의미하므로 if (airport == default(Airport) { throw ... }인스턴스 Airport(및 그 Code속성)를 얻는 것이 작업 에 중요한 곳에서하고 있습니다.

참고 : C # / VB struct – 주어진 구조에 유효하지 않은 기본값이 0 인 사례를 피하는 방법을 검토했습니다. 내 질문을하기 전에 struct사용하거나 사용하지 않지만 , 내 질문은 자체 게시물을 보증하기에 충분히 다르다고 생각합니다.


7
가비지 콜렉션이 애플리케이션 수행 방식에 중대한 영향을 미칩니 까 ? 다시 말해서, 그것은 중요합니까?
Robert Harvey

어쨌든, 클래스 솔루션은 "좋은"솔루션이었습니다. 당신이 그것을 아는 방법은 새로운 것을 만들지 않고 문제를 해결하는 것입니다.
Robert Harvey

2
default(Airport)문제를 해결할 수있는 한 가지 방법 은 단순히 기본 인스턴스를 허용하지 않는 것입니다. 매개 변수가없는 생성자를 작성하고 던지 InvalidOperationException거나 던져서 그렇게 할 수 있습니다 NotImplementedException.
Robert Harvey

3
참고로, 초기화 문자열이 실제로 3 개의 알파벳 문자임을 확인하는 대신, 모든 공항 코드의 유한 목록 (예 : github.com/datasets/airport-codes 또는 이와 유사한)과 비교해 보 시겠습니까?
Dan Pichelman

2
나는 이것이 성능 문제의 근본이 아니라는 몇 가지 맥주에 베팅 할 것입니다. 일반 랩톱은 10M 개체 / 초의 순서로 할당 할 수 있습니다.
Esben Skov Pedersen

답변:


6

업데이트 : C # 구조체에 대한 잘못된 가정과 내부 문자열이 사용되고 있다는 의견에 OP를 알려주는 답변을 다시 작성했습니다.


시스템으로 들어오는 데이터를 제어 할 수 있다면 질문에 게시 한 클래스를 사용하십시오. 누군가가 실행되면 default(Airport)그들은 얻을 것이다 null값 다시. Equalsnull Airport 객체를 비교할 때마다 false를 반환하도록 개인 메서드 를 작성한 다음 NullReferenceException코드의 다른 곳에서 비행 하도록하십시오 .

그러나 제어하지 않는 소스에서 시스템으로 데이터를 가져 오는 경우 전체 스레드를 중단하지 않아도됩니다. 이 경우 구조체는 단순한 사실에 이상적이며 포인터 default(Airport)이외의 것을 제공합니다 null. "값 없음"또는 "기본값"을 나타내는 명백한 값을 구성하여 화면이나 로그 파일 (예 : "---")에 인쇄 할 내용을 갖도록하십시오. 사실, 나는 code개인을 유지하고 Code재산을 전혀 노출시키지 않고 행동에 중점을 둡니다.

public struct Airport
{
    private string code;

    public Airport(string code)
    {
        // Check `code` for validity, throw exceptions if not valid

        this.code = code;
    }

    public override string ToString()
    {
        return code ?? (code = "---");
    }

    // int GetHashcode()

    // bool Equals(...)

    // bool operator ==(...)

    // bool operator !=(...)

    private bool Equals(Airport other)
    {
        if (other == null)
            // Even if this method is private, guard against null pointers
            return false;

        if (ToString() == "---" || other.ToString() == "---")
            // "Default" values should never match anything, even themselves
            return false;

        // Do a case insensitive comparison to enforce logic that airport
        // codes are not case sensitive
        return string.Equals(
            ToString(),
            other.ToString(),
            StringComparison.InvariantCultureIgnoreCase);
    }
}

default(Airport)문자열 로 변환 하는 최악의 시나리오 "---"는 다른 유효한 공항 코드와 비교하여 인쇄되고 false를 반환합니다. "기본"공항 코드는 다른 기본 공항 코드를 포함하여 아무 것도 일치하지 않습니다.

예, 구조체는 스택에 할당 된 값을 의미하며 힙 메모리에 대한 포인터는 기본적으로 구조체의 성능 이점을 무효화하지만이 경우 구조체의 기본값은 의미를 가지며 나머지 부분에 총알 저항을 추가로 제공합니다 신청.

그 때문에 규칙을 약간 구부릴 것입니다.


원래 답변 (사실상 오류가 있음)

시스템으로 들어오는 데이터를 제어 할 수 있다면 Robert Harvey는 다음과 같이 주석에서 제안한대로 수행합니다. 매개 변수가없는 생성자를 만들고 호출 될 때 예외를 throw합니다. 이를 통해 잘못된 데이터가 시스템을 통해 시스템에 입력되는 것을 방지 default(Airport)합니다.

public Airport()
{
    throw new InvalidOperationException("...");
}

그러나 제어하지 않는 소스에서 시스템으로 데이터를 가져 오는 경우 전체 스레드를 중단하지 않아도됩니다. 이 경우 유효하지 않은 공항 코드를 만들 수 있지만 명백한 오류처럼 보일 수 있습니다. 여기에는 매개 변수가없는 생성자를 만들고 Code"---"와 같은 것을 설정하는 것이 포함됩니다 .

public Airport()
{
    Code = "---";
}

당신은을 사용하고 있기 때문에 string코드로, 구조체를 사용하여 아무 문제가 없다. 구조체는 스택에 Code할당되며 힙 메모리의 문자열에 대한 포인터 로만 할당되므로 클래스와 구조체의 차이점은 없습니다.

공항 코드를 char의 3 항목 배열로 변경하면 구조체가 스택에 완전히 할당됩니다. 그럼에도 불구하고 데이터의 양은 큰 차이가 없습니다.


내 응용 프로그램이 Code속성에 인터 닝 된 문자열을 사용하는 경우 힙 메모리에있는 문자열의 요점에 대한 정당성이 변경됩니까?
Matthew

@ Matthew : 클래스를 사용하여 성능 문제가 있습니까? 그렇지 않은 경우 동전을 뒤집어 사용할 것을 결정하십시오.
Greg Burghardt

4
@Matthew : 실제로 중요한 것은 코드와 비교를 표준화하는 귀찮은 논리를 중앙 집중화하는 것입니다. 그 후 "class vs struct"는 학술 토론 일뿐입니다. 개발자가 아카데믹 토론을 할 수있는 추가 시간을 정당화 할 수있을 정도로 성능에 충분히 큰 영향을 줄 때까지.
Greg Burghardt

1
그것이 사실, 미래에 더 나은 정보 솔루션을 만드는 데 도움이된다면 때때로 학술 토론을하는 것이 마음에 들지 않습니다.
Matthew

@ 매튜 : 네, 당신은 절대적으로 맞습니다. 그들은 "대화가 싸다"고 말합니다. 말을 잘 못하고 무언가를 잘못 짓는 것보다 확실히 저렴합니다. :)
Greg Burghardt

13

사용 플라이급 패턴을

공항은 정확하고 불변이므로 SFO와 같은 특정 인스턴스의 인스턴스를 두 개 이상 만들 필요가 없습니다. Hashtable 또는 이와 유사한 (참고로 C #이 아니므로 정확한 세부 사항이 다를 수 있음) Java를 사용하여 공항을 만들 때 캐시하십시오. 새 것을 만들기 전에 해시 테이블을 체크인하십시오. 공항을 확보하지 않으니 GC는 공항을 해제 할 필요가 없습니다.

최소한의 Java (C #에 대해서는 확실하지 않음)의 또 하나의 작은 장점은 equals()메소드 를 작성할 필요가 없다는 ==것입니다. 동일합니다 hashcode().


3
플라이급 패턴의 뛰어난 사용법.
Neil

2
OP가 클래스가 아닌 구조체를 계속 사용한다고 가정하면 문자열 인턴은 이미 재사용 가능한 문자열 값을 처리 하지 않습니까? 구조체는 이미 스택에 있으며 문자열은 메모리에서 중복 값을 피하기 위해 이미 재사용되고 있습니다. 플라이급 패턴에서 어떤 추가 혜택을 얻을 수 있습니까?
Flater

조심해야 할 것. 공항이 추가 또는 제거 된 경우 응용 프로그램을 다시 시작하거나 재배치하지 않고이 정적 목록을 새로 고치는 방법을 만들고 싶을 것입니다. 공항은 자주 추가되거나 제거되지 않지만 간단한 변경이 복잡해지면 비즈니스 소유자가 약간 화를내는 경향이 있습니다. "어딘가에 추가 할 수 없습니까? 왜 릴리스 / 응용 프로그램 재시작을 예약하고 고객에게 불편을 줍니까?" 그러나 처음에는 일종의 정적 캐시를 사용하려고 생각했습니다.
Greg Burghardt

@Flater 합리적인 지점. 주니어 프로그래머가 스택 대 힙에 대해 추론 할 필요가 덜하다고 말하고 싶습니다. 또한 내 추가 사항을 참조하십시오-equals ()를 작성할 필요가 없습니다.
user949300

1
@Greg Burghardt getAirportOrCreate()코드가 올바르게 동기화 된 경우 런타임 중에 필요에 따라 새 공항을 만들 수 없는 기술적 이유가 없습니다. 사업상의 이유가있을 수 있습니다.
user949300

3

나는 특별히 고급 프로그래머는 아니지만 Enum에 완벽하게 사용되지 않습니까?

목록이나 문자열에서 열거 형 클래스를 구성하는 방법에는 여러 가지가 있습니다. 과거에 본 적이 있지만 이것이 최선의 방법인지 확실하지 않습니다.

https://blog.kloud.com.au/2016/06/17/converting-webconfig-values-into-enum-or-list/


2
공항 코드와 마찬가지로 수천 개의 다른 값이있을 때 열거 형은 실용적이지 않습니다.
Ben Cottrell

예, 그러나 게시 한 링크는 문자열을 열거 형으로로드하는 방법입니다. 조회 테이블을 열거 형으로로드하는 또 다른 링크가 있습니다. 약간의 작업 일 수도 있지만 열거 형의 힘을 활용할 것입니다. exceptionnotfound.net/…
Adam B

1
또는 데이터베이스 나 파일에서 유효한 코드 목록을로드 할 수 있습니다. 그런 다음 공항 코드가 해당 목록에 있는지 확인합니다. 이것은 더 이상 값을 하드 코딩하고 싶지 않거나 목록을 관리하는 데 오랜 시간이 걸리는 경우에 일반적으로 수행하는 작업입니다.
Neil

@BenCottrell은 코드 생성 및 T4 템플릿의 용도입니다.
RubberDuck

3

더 많은 GC 활동을 보는 이유 중 하나는 지금 .ToUpperInvariant()원래 문자열 의 버전 인 두 번째 문자열을 생성하기 때문 입니다. 원래 문자열은 생성자가 실행 된 직후 GC에 적합하고 두 번째 문자열은 Airport객체 와 동시에 적합 합니다. 다른 방식으로 최소화 할 수 있습니다 (세 번째 매개 변수에 대한 참고 string.Equals()) :

public sealed class Airport : IEquatable<Airport>
{
    public Airport(string code)
    {
        if (code == null)
        {
            throw new ArgumentNullException(nameof(code));
        }

        if (code.Length != 3 || !char.IsLetter(code[0])
                             || !char.IsLetter(code[1]) || !char.IsLetter(code[2]))
        {
            throw new ArgumentException(
                "Must be a 3 letter airport code.",
                nameof(code));
        }

        Code = code;
    }

    public string Code { get; }

    public override string ToString()
    {
        return Code; // TODO: Upper-case it here if you really need to for display.
    }

    public bool Equals(Airport other)
    {
        return string.Equals(Code, other?.Code, StringComparison.InvariantCultureIgnoreCase);
    }

    public override bool Equals(object obj)
    {
        return obj is Airport airport && Equals(airport);
    }

    public override int GetHashCode()
    {
        return Code.GetHashCode();
    }

    public static bool operator ==(Airport left, Airport right)
    {
        return Equals(left, right);
    }

    public static bool operator !=(Airport left, Airport right)
    {
        return !Equals(left, right);
    }
}

이것이 동일한 (그러나 다른 대문자로 된) 공항에 대해 다른 해시 코드를 생성하지 않습니까?
Hero Wanders

네, 그렇게 생각합니다. 단깃.
Jesse C. Slicer

이것은 아주 좋은 지적입니다. 한번도 생각해 본 적이 없습니다. 저는 이러한 변화를 살펴볼 것입니다.
Matthew

1
에 관해서는 GetHashCode, 그냥 사용 StringComparer.OrdinalIgnoreCase.GetHashCode(Code)하거나 유사 해야합니다
매튜
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.