"그룹화"열거 형에 플래그를 사용하는 것이 잘못 되었습니까?


12

내 이해는 [Flag]열거 형은 일반적으로 개별 값이 상호 배타적이지 않은 결합 될 수있는 것들에 사용된다는 것입니다 .

예를 들면 다음과 같습니다.

[Flags]
public enum SomeAttributes
{
    Foo = 1 << 0,
    Bar = 1 << 1,
    Baz = 1 << 2,
}

어디에서 어떤 SomeAttributes값의 조합이 될 수 있습니다 Foo, Bar하고 Baz.

더 복잡한 실제 시나리오 에서는 열거 형을 사용하여 다음을 설명합니다 DeclarationType.

[Flags]
public enum DeclarationType
{
    Project = 1 << 0,
    Module = 1 << 1,
    ProceduralModule = 1 << 2 | Module,
    ClassModule = 1 << 3 | Module,
    UserForm = 1 << 4 | ClassModule,
    Document = 1 << 5 | ClassModule,
    ModuleOption = 1 << 6,
    Member = 1 << 7,
    Procedure = 1 << 8 | Member,
    Function = 1 << 9 | Member,
    Property = 1 << 10 | Member,
    PropertyGet = 1 << 11 | Property | Function,
    PropertyLet = 1 << 12 | Property | Procedure,
    PropertySet = 1 << 13 | Property | Procedure,
    Parameter = 1 << 14,
    Variable = 1 << 15,
    Control = 1 << 16 | Variable,
    Constant = 1 << 17,
    Enumeration = 1 << 18,
    EnumerationMember = 1 << 19,
    Event = 1 << 20,
    UserDefinedType = 1 << 21,
    UserDefinedTypeMember = 1 << 22,
    LibraryFunction = 1 << 23 | Function,
    LibraryProcedure = 1 << 24 | Procedure,
    LineLabel = 1 << 25,
    UnresolvedMember = 1 << 26,
    BracketedExpression = 1 << 27,
    ComAlias = 1 << 28
}

분명히 주어진 값은 Declarationa Variable와 a 일 LibraryProcedure수 없습니다. 두 개의 개별 값은 결합 될 수 없습니다.

이러한 플래그는 매우 유용하지만 (그것은 주어진이 있는지 여부를 확인하는 것은 매우 쉬운 DeclarationTypeA는 Property또는를 Module플래그가되지 않기 때문에, 그것은 "잘못"느낌) 정말 사용 결합 값을, 오히려위한 그룹화 "하위 유형"로한다.

그래서 이것이 열거 형 플래그를 남용한다고 들었습니다. 이 대답은 본질적으로 사과에 적용 가능한 값 세트와 오렌지에 적용 가능한 다른 세트가있는 경우 사과에 대해 다른 열거 형과 오렌지에 대해 다른 열거 형이 필요하다는 것을 나타냅니다- 여기서 문제 DeclarationType는 기본 Declaration클래스 에 노출 된 공통 인터페이스를 갖는 모든 선언이 필요하다는 PropertyType것입니다 . 열거 형을 갖는 것은 전혀 유용하지 않습니다.

조잡하거나 놀랍거나 설득력있는 디자인입니까? 그렇다면 그 문제는 일반적으로 어떻게 해결됩니까?


사람들이 유형의 객체를 어떻게 사용할지 고려하십시오 DeclarationType. 내가 여부를 결정하려는 경우 x의 하위 유형은 y, 아마 같은 것을 쓰고 싶은거야 x.IsSubtypeOf(y)하지, x && y == y.
Tanner Swett

1
@TannerSwett 그의 아주 정확히 x.HasFlag(DeclarationType.Member)않습니다 ....
마티유 Guindon

그렇습니다. 그러나 메소드 HasFlag대신에 메소드를 호출 하면 실제로 의미하는 것이 "하위 유형"이라는 것을 알 수있는 다른 방법이 IsSubtypeOf필요합니다 . 확장 메서드를 만들 수는 있지만 사용자로서 가장 놀라운 것은 실제 메서드 와 같은 구조체가 되는 것입니다. DeclarationTypeIsSubtypeOf
Tanner Swett

답변:


10

이것은 분명히 열거 형과 플래그를 남용합니다! 그것은 당신을 위해 일할 수도 있지만 코드를 읽는 다른 사람은 크게 혼란 스러울 것입니다.

내가 올바르게 이해하면 계층 적 선언 분류 가 있습니다. 이다 지금까지 단일 열거의 인코딩에 많은 정보를. 그러나 클래스와 상속을 사용하는 확실한 대안이 있습니다! 그래서 Member에서 상속 DeclarationType, Property상속에서 Member등등.

열거 형은 특정 상황에서 적합합니다. 값이 항상 제한된 수의 옵션 중 하나이거나 제한된 수의 옵션 (플래그)의 조합 인 경우. 이보다 더 복잡하거나 구조화 된 정보는 객체를 사용하여 표현해야합니다.

편집 : "실제 시나리오" 에서는 열거 형 값에 따라 동작이 선택 되는 여러 위치가있는 것 같습니다 . switch+ enum를 "가난한 사람 다형성"으로 사용하기 때문에 이것은 실제로 반 패턴 입니다. 열거 형 값을 선언 별 동작을 캡슐화하는 개별 클래스로 바꾸면 코드가 훨씬 깨끗해집니다.


상속은 좋은 생각이지만 당신의 대답은 열거 형 당 클래스를 암시합니다.
Frank Hileman

@FrankHileman : 계층 구조의 "리프"는 클래스가 아닌 열거 형 값으로 표시 될 수 있지만 더 나을지 확신 할 수 없습니다. 다른 열거 형 값으로 다른 동작이 연결되는지 여부에 따라 다르면 별도의 클래스가 더 좋습니다.
JacquesB

4
분류는 계층 적이 지 않으며 조합은 사전 정의 된 상태입니다. 이를 위해 상속을 사용하는 것은 진정 남용, 즉 남용입니다. 클래스를 사용하면 적어도 일부 데이터 또는 동작이 필요합니다. 어느 쪽도없는 클래스는 ... 맞아, 열거 형입니다.
Martin Maat

내 리포지토리에 대한 링크 정보 : 유형별 동작은 ParserRuleContext열거 형보다 생성 된 클래스 유형과 더 관련이 있습니다. 이 코드는 As {Type}선언에 절 을 삽입 할 위치를 토큰 위치로 가져 오려고합니다. 이 ParserRuleContext파생 클래스는 파서 규칙을 정의하는 문법마다 Antr4에 의해 생성됩니다. 파스 트리 노드의 [더 얕은] 상속 계층 구조를 실제로 제어하지는 않지만 partial인터페이스를 구현하기 위해 -ness를 활용할 수는 있지만 그들이 어떤 AsTypeClauseTokenPosition재산을 폭로하도록하세요 . 많은 일.
Mathieu Guindon

6

이 접근법은 읽고 이해하기에 충분히 쉽다는 것을 알았습니다. IMHO, 그것은 혼란스러워 할 것이 아닙니다. 그러나 나는이 접근법에 대해 몇 가지 우려를 가지고 있습니다.

  1. 나의 주요 예약은 이것을 시행 할 수있는 방법이 없다는 것입니다.

    분명히 주어진 선언은 변수 및 라이브러리 프로 ​​시저가 될 수 없습니다 . 두 개의 개별 값 은 결합 될 수 없습니다.

    위의 조합을 선언하지는 않지만이 코드

    var oops = DeclarationType.Variable | DeclarationType.LibraryProcedure;

    완벽하게 유효합니다. 컴파일 타임에 이러한 오류를 잡을 방법이 없습니다.

  2. 비트 플래그로 인코딩 할 수있는 정보의 양에는 한계가 있습니다. 64 비트는 무엇입니까? 지금은 위험에 가까워지고 int있으며이 열거 형이 계속 커지면 결국 비트가 부족할 수 있습니다 ...

결론은 이것이 유효한 접근법이라고 생각하지만 크고 복잡한 계층의 플래그 에이 방법을 사용하는 것을 주저합니다.


따라서 파서 단위 테스트를 철저히 수행하면 1 위가됩니다. 이제 열거 형은 약 3 년 전에 표준 비 플래그 열거 형으로 시작했습니다. 열거 형 플래그가 나타나는 특정 값 (예 : 코드 검사)을 항상 확인하는 데 지 치면서 성장했습니다. 이 목록은 시간이 지남에 따라 실제로 증가하지는 않지만 int그래도 용량을 꺾는 것은 실제 관심사입니다.
Mathieu Guindon

3

TL; DR 맨 아래로 스크롤하십시오.


내가 본 것에서 C # 위에 새로운 언어를 구현하고 있습니다. 열거 형은 식별자의 유형 (또는 이름이 있고 새로운 언어의 소스 코드에 나타나는 것)을 나타내는 것으로 보이며, 이는 프로그램의 트리 표현에 추가 될 노드에 적용되는 것으로 보입니다.

이 특정 상황에서는 서로 다른 유형의 노드 사이에 다형성 동작이 거의 없습니다. 나무가 매우 다른 종류의 (변형)의 노드를 포함 할 수 있어야하는 것이 필요하다 동안 즉,이 노드의 실제 방문은 기본적으로 거대한 경우 - 당시 다른 체인 (또는에 의존합니다 instanceof/ is확인). 이 거대한 수표는 프로젝트의 여러 곳에서 일어날 것입니다. 이것이 열거 형이 도움이 될 수있는 이유이거나 적어도 확인 instanceof/ is확인 만큼 도움이되는 이유 입니다.

방문자 패턴 이 여전히 유용 할 수 있습니다. 즉,의 거대한 체인 대신 사용할 수있는 다양한 코딩 스타일이 있습니다 instanceof. 그러나 다양한 이점과 단점에 대한 토론을 원한다면 instanceof열거 형에 대해 혼란스러워하는 대신 프로젝트에서 가장 추악한 체인의 코드 예제를 보여 주려고했을 것입니다.

이것은 클래스와 상속 계층이 유용하지 않다는 것은 아닙니다. 정반대. 모든 선언 유형에 걸쳐 작동하는 다형성 동작은 없지만 (모든 선언에는 Name속성이 있어야한다는 사실은 제외 ) 인근 형제들이 공유하는 풍부한 다형성 동작이 많이 있습니다. 예를 들어, Function그리고 Procedure아마도 몇 가지 행동을 (모두 호출되는 및 입력 된 입력 인수 목록을 받아들이) 공유하고 PropertyGet의지에서 확실히 상속 행동 Function(모두가를 가진 ReturnType). 거대한 if-then-else 체인에 대해 열거 형 또는 상속 검사를 사용할 수 있지만 조각화되었지만 다형성 동작은 여전히 ​​클래스에서 구현되어야합니다.

남용 instanceof/ is확인 에 대한 많은 온라인 조언이 있습니다 . 성능은 그 이유 중 하나가 아닙니다. 오히려, 그 이유는 유기 것처럼, 적절한 행동 다형성을 발견으로부터 프로그래머를 방지하기 위함이다 instanceof/ is버팀목이다. 그러나이 노드는 공통점이 거의 없으므로 상황에 따라 다른 선택이 없습니다.

다음은 구체적인 제안입니다.


비 리프 그룹화를 나타내는 몇 가지 방법이 있습니다.


원래 코드의 다음 발췌 부분을 비교하십시오 ...

[Flags]
public enum DeclarationType
{
    Member = 1 << 7,
    Procedure = 1 << 8 | Member,
    Function = 1 << 9 | Member,
    Property = 1 << 10 | Member,
    PropertyGet = 1 << 11 | Property | Function,
    PropertyLet = 1 << 12 | Property | Procedure,
    PropertySet = 1 << 13 | Property | Procedure,
    LibraryFunction = 1 << 23 | Function,
    LibraryProcedure = 1 << 24 | Procedure,
}

이 수정 된 버전으로 :

[Flags]
public enum DeclarationType
{
    Nothing = 0, // to facilitate bit testing

    // Let's assume Member is not a concrete thing, 
    // which means it doesn't need its own bit
    /* Member = 1 << 7, */

    // Procedure and Function are concrete things; meanwhile 
    // they can still have sub-types.
    Procedure = 1 << 8, 
    Function = 1 << 9, 
    Property = 1 << 10,

    PropertyGet = 1 << 11,
    PropertyLet = 1 << 12,
    PropertySet = 1 << 13,

    LibraryFunction = 1 << 23,
    LibraryProcedure = 1 << 24,

    // new
    Procedures = Procedure | PropertyLet | PropertySet | LibraryProcedure,
    Functions = Function | PropertyGet | LibraryFunction,
    Properties = PropertyGet | PropertyLet | PropertySet,
    Members = Procedures | Functions | Properties,
    LibraryMembers = LibraryFunction | LibraryProcedure 
}

이 수정 된 버전은 비 콘크리트 선언 유형에 비트를 할당하지 않습니다. 대신, 비 콘크리트 선언 유형 (선언 유형의 추상 그룹)은 단순히 모든 자식에 대해 비트 단위 또는 비트 단위의 열거 형 값을 갖습니다.

하나의 자식이있는 추상 선언 유형이 있고 추상적 인 것 (부모)과 구체적인 것 (자식)을 구별 할 필요가있는 경우, 추상적 인 것에는 여전히 고유 한 비트가 필요합니다 .


이 질문에 대한 한 가지주의 사항 : a Property는 처음에는 식별자입니다 (코드에서 어떻게 사용되는지 보지 않고 이름 만 보았을 때), 어떻게 사용되는지 보자 마자 PropertyGet/ PropertyLet/ PropertySet로 변환 될 수 있습니다 코드에서. 즉, 구문 분석의 여러 단계에서 Property식별자를 "이 이름은 속성을 나타냅니다"로 표시 한 다음 나중에 "이 코드 행은 특정 방식으로이 속성에 액세스하고 있습니다"로 변경해야 할 수 있습니다.

이 경고를 해결하려면 두 세트의 열거 형이 필요할 수 있습니다. 하나의 열거 형은 이름 (식별자)이 무엇인지를 나타냅니다. 또 다른 열거 형은 코드가 수행하려는 작업을 나타냅니다 (예 : 무언가의 본문 선언, 특정 방식으로 무언가 사용하려고 함).


대신 각 열거 형 값에 대한 보조 정보를 배열에서 읽을 수 있는지 고려하십시오.

이 제안은 2의 거듭 제곱 값을 작은 음이 아닌 정수 값으로 다시 변환해야하기 때문에 다른 제안과 상호 배타적입니다.

public enum DeclarationType
{
    Procedure = 8,
    Function = 9,
    Property = 10,
    PropertyGet = 11,
    PropertyLet = 12,
    PropertySet = 13,
    LibraryFunction = 23,
    LibraryProcedure = 24,
}

static readonly bool[] DeclarationTypeIsMember = new bool[32]
{
    ?, ?, ?, ?, ?, ?, ?, ?,                   // bit[0] ... bit[7]
    true, true, true, true, true, true, ?, ?, // bit[8] ... bit[15]
    ?, ?, ?, ?, ?, ?, ?, true,                // bit[16] ... bit[23]
    true, ...                                 // bit[24] ... 
}

static bool IsMember(DeclarationType dt)
{
    int intValue = (int)dt;
    return (intValue < 0 || intValue >= 32) ? false : DeclarationTypeIsMember[intValue];
    // you can also throw an exception if the enum is outside range.
}

// likewise for IsFunction(dt), IsProcedure(dt), IsProperty(dt), ...

유지 보수성이 문제가 될 것입니다.


C # 형식 (상속 계층의 클래스)과 열거 형 값 사이의 일대일 매핑 여부를 확인하십시오.

(또는 유형과 일대일 매핑을 보장하기 위해 열거 형 값을 조정할 수 있습니다.)

C #에서는 많은 라이브러리 Type object.GetType()가 좋은 방법으로 나쁜 방법을 악용합니다 .

열거 형을 값으로 저장하는 모든 곳에서 Type대신 값으로 저장할 수 있는지 여부를 스스로에게 묻을 수 있습니다 .

이 트릭을 사용하기 위해 두 가지 읽기 전용 해시 테이블을 초기화 할 수 있습니다.

// For disambiguation, I'll assume that the actual 
// (behavior-implementing) classes are under the 
// "Lang" namespace.

static readonly Dictionary<Type, DeclarationType> TypeToDeclEnum = ... 
{
    { typeof(Lang.Procedure), DeclarationType.Procedure },
    { typeof(Lang.Function), DeclarationType.Function },
    { typeof(Lang.Property), DeclarationType.Property },
    ...
};

static readonly Dictionary<DeclarationType, Type> DeclEnumToType = ...
{
    // same as the first dictionary; 
    // just swap the key and the value
    ...
};

클래스와 상속 계층 구조를 제안하는 사람들을위한 최종 검증

열거 형이 상속 계층 구조에 대한 근사치 임을 알면 다음 조언이 유지됩니다.

  • 상속 계층 구조를 먼저 디자인 (또는 개선)하고
  • 그런 다음 돌아가서 해당 상속 계층 구조와 비슷한 열거 형을 디자인하십시오.

내가 VBA 코드 =) 구문 분석 및 분석하고 있습니다 -이 프로젝트는 VBIDE 추가 기능 실제로
마티유 Guindon

1

나는 당신의 깃발 사용이 정말 똑똑하고 창의적이며 우아하고 잠재적으로 가장 효율적이라고 생각합니다. 나는 그것을 읽는 데 전혀 문제가 없습니다.

플래그는 신호 상태, 자격의 수단입니다. 과일이 무엇인지 알고 싶다면

thingy & Organic.Fruit! = 0

보다 읽기 쉬운

thingy & (Organic.Apple | Organic.Orange | Organic.Pear)! = 0

플래그 열거의 요점은 여러 상태를 결합 할 수 있도록하는 것입니다. 이것을 더 유용하고 읽기 쉽게 만들었습니다. 당신은 당신의 코드에서 과일의 개념을 전달합니다, 나는 사과와 오렌지와 배가 과일을 의미한다는 것을 스스로 알아낼 필요가 없습니다.

이 남자에게 브라우니 포인트를 줘!


4
thingy is Fruit그래도 더 읽기 쉽습니다.
JacquesB
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.