`goto ‘부두를 피하는가? FourTwo, FourThree, FourOneTwo,

switch처리해야 할 몇 가지 사례 가있는 구조가 있습니다. 는 switch과도하게 작동 enum값 조합을 통해 중복 된 코드의 문제를 제기한다 :

// All possible combinations of One - Eight.
public enum ExampleEnum {
    One,
    Two, TwoOne,
    Three, ThreeOne, ThreeTwo, ThreeOneTwo,
    Four, FourOne, FourTwo, FourThree, FourOneTwo, FourOneThree,
          FourTwoThree, FourOneTwoThree
    // ETC.
}

현재 switch구조는 각 값을 개별적으로 처리합니다.

// All possible combinations of One - Eight.
switch (enumValue) {
    case One: DrawOne; break;
    case Two: DrawTwo; break;
    case TwoOne:
        DrawOne;
        DrawTwo;
        break;
     case Three: DrawThree; break;
     ...
}

거기에서 아이디어를 얻습니다. 나는 현재 이것을 if한 줄로 조합을 처리하기 위해 스택 구조 로 세분화했습니다 .

// All possible combinations of One - Eight.
if (One || TwoOne || ThreeOne || ThreeOneTwo)
    DrawOne;
if (Two || TwoOne || ThreeTwo || ThreeOneTwo)
    DrawTwo;
if (Three || ThreeOne || ThreeTwo || ThreeOneTwo)
    DrawThree;

이것은 읽기에 혼란스럽고 유지하기 어려운 논리적 인 평가의 문제를 제기합니다. 이것을 리팩토링 한 후에 나는 대안들에 대해 생각하기 시작했고 switch사례들 사이에 틀에 박힌 구조 에 대한 아이디어를 생각하기 시작했다 .

폴 스루를 허용하지 않으므로이 goto경우에는를 사용해야합니다 C#. 그러나 switch구조 에서 뛰어 넘더라도 엄청나게 긴 논리 체인을 방지하고 여전히 코드 복제를 가져옵니다.

switch (enumVal) {
    case ThreeOneTwo: DrawThree; goto case TwoOne;
    case ThreeTwo: DrawThree; goto case Two;
    case ThreeOne: DrawThree; goto default;
    case TwoOne: DrawTwo; goto default;
    case Two: DrawTwo; break;
    default: DrawOne; break;
}

이것은 여전히 ​​충분히 깨끗하지 않은 솔루션이며 goto피하고 싶은 키워드 와 관련된 낙인이 있습니다 . 이것을 정리하는 더 좋은 방법이 있어야합니다.


내 질문

가독성과 유지 관리성에 영향을 미치지 않으면 서이 특정 사례를 처리하는 더 좋은 방법이 있습니까?



답변

goto문장으로 코드를 읽기가 어렵다는 것을 알았습니다 . 나는 당신을 enum다르게 구성하는 것이 좋습니다 . 예를 들어, enum각 비트가 선택 사항 중 하나를 나타내는 비트 필드 인 경우 다음과 같이 보일 수 있습니다.

[Flags]
public enum ExampleEnum {
    One = 0b0001,
    Two = 0b0010,
    Three = 0b0100
};

Flags 속성은 컴파일러에게 겹치지 않는 값을 설정하고 있음을 알려줍니다. 이 코드를 호출하는 코드는 적절한 비트를 설정할 수 있습니다. 그런 다음 무슨 일이 일어나고 있는지 명확하게하기 위해 이와 같은 작업을 수행 할 수 있습니다.

if (myEnum.HasFlag(ExampleEnum.One))
{
    CallOne();
}
if (myEnum.HasFlag(ExampleEnum.Two))
{
    CallTwo();
}
if (myEnum.HasFlag(ExampleEnum.Three))
{
    CallThree();
}

myEnum비트 필드를 올바르게 설정하고 Flags 속성으로 표시된 코드가 필요 합니다. 그러나 예제의 열거 형 값을 다음과 같이 변경하여이를 수행 할 수 있습니다.

[Flags]
public enum ExampleEnum {
    One = 0b0001,
    Two = 0b0010,
    Three = 0b0100,
    OneAndTwo = One | Two,
    OneAndThree = One | Three,
    TwoAndThree = Two | Three
};

형식으로 숫자를 쓰면 0bxxxx이진수로 지정됩니다. 그래서 우리는 비트 1, 2 또는 3을 설정하는 것을 볼 수 있습니다 (기술적으로 0, 1 또는 2이지만 아이디어를 얻습니다). 조합이 자주 설정되는 경우 비트 단위 OR을 사용하여 조합의 이름을 지정할 수도 있습니다.


답변

문제의 근본 원인은이 코드가 존재하지 않아야한다는 것입니다.

당신은 분명히 세 개의 독립적 인 조건을 가지고 있으며, 그러한 조건이 참이라면 취할 세 가지 독립적 인 조치를 취합니다. 그렇다면 왜 모든 것이 하나의 코드 조각으로 퍼져서 무엇을 해야하는지 알려주기 위해 세 개의 부울 플래그가 필요하고 (열거에 열거하지 않든 상관없이) 세 개의 독립적 인 것들을 결합합니까? 단일 책임 원칙은 여기서 하루를 보낸 것 같습니다.

소속 된 (즉, 조치를 수행해야 할 필요성을 발견 한) 세 가지 함수를 호출하고이 예제의 코드를 휴지통에 위탁하십시오.

3 개가 아닌 10 개의 플래그와 동작이있는 경우 1024 가지의 다른 조합을 처리하도록이 코드를 확장 하시겠습니까? 내가하지 희망! 1024가 너무 많으면 같은 이유로 8도 너무 많습니다.


답변

gotos를 사용하지 마십시오 는 컴퓨터 과학의 “어린이에게 거짓말” 개념 중 하나입니다 . 시간의 99 %가 올바른 조언이며, 너무 드물고 특별한 시간이 아니므로 새로운 코더에게 “사용하지 마십시오”라고 설명하면 모든 사람에게 훨씬 좋습니다.

그래서 언제 사용해야 합니까? 있다 몇 가지 시나리오가 있지만 타격 것 같다 기본적인 일이다 : 당신이 상태 머신 (state machine)을 코딩 할 때 . 상태 머신보다 더 체계적이고 체계적인 알고리즘 표현이 없다면 코드의 자연 표현에는 구조화되지 않은 브랜치가 포함되며, 논란의 여지가없는 구조에 대해서는 할 수있는 일이 많지 않습니다. 상태 머신 자체 보다 오히려 이하, 무명.

컴파일러 작가는 LALR 파서를 구현하는 대부분의 컴파일러의 소스 코드가 이유입니다, 이것을 알고 * gotos이 포함되어 있습니다. 그러나 실제로 자신의 어휘 분석기와 파서를 코딩하는 사람은 거의 없습니다.

* IIRC, 테이블 또는 기타 구조화되지 않은 제어문을 사용하지 않고 LALL 문법을 완전히 재귀 강등 적으로 구현할 수 있으므로 진정으로 반 고토 (anti-goto) 인 경우이 방법을 사용할 수 있습니다.


다음 질문은 “이 예제는 그 중 하나입니까?”입니다.

내가보고있는 것은 현재 상태의 처리에 따라 세 가지 가능한 다음 상태가 있다는 것입니다. 그중 하나 ( “default”)는 코드의 한 줄에 불과하기 때문에 기술적으로 해당 코드의 끝 부분에 해당 코드를 적용하여 제거 할 수 있습니다. 그러면 다음 두 가지 상태로 넘어갈 수 있습니다.

남은 것 중 하나 ( “Three”)는 내가 볼 수있는 한 곳에서만 나옵니다. 따라서 같은 방식으로 제거 할 수 있습니다. 다음과 같은 코드가 생깁니다.

switch (exampleValue) {
    case OneAndTwo: i += 3 break;
    case OneAndThree: i += 4 break;
    case Two: i += 2 break;
    case TwoAndThree: i += 5 break;
    case Three: i += 3 break;
    default: i++ break;
}

그러나 이것은 다시 당신이 제공 한 장난감 사례였습니다. “기본”그것은 코드의 적지 않은 양이 경우에서는 “세”가 여러 국가에서로 전환되거나 (가장 중요) 더 유지 관리 -이 상태를 추가하거나 복잡 가능성 , 솔직히 더 나을 것 gotos 사용 (그리고 아마도 머무를 필요가없는 좋은 이유가 없다면 사물의 상태 기계 특성을 숨기는 열거 형 구조를 제거 할 수도 있습니다).


답변

가장 좋은 대답은 다형성을 사용하는 것입니다 .

또 다른 대답, IMO는 if 물건을보다 명확 하고 논쟁의 여지없이 짧게 만듭니다 .

if (One || OneAndTwo || OneAndThree)
  CallOne();
if (Two || OneAndTwo || TwoAndThree)
  CallTwo();
if (Three || OneAndThree || TwoAndThree)
  CallThree();

goto 아마도 내 58 번째 선택 일 것입니다 …


답변

왜 그렇지 않습니까?

public enum ExampleEnum {
    One = 0, // Why not?
    OneAndTwo,
    OneAndThree,
    Two,
    TwoAndThree,
    Three
}
int[] COUNTS = { 1, 3, 4, 2, 5, 3 }; // Whatever

int ComputeExampleValue(int i, ExampleEnum exampleValue) {
    return i + COUNTS[(int)exampleValue];
}

좋아, 나는 이것이 해킹 적이라고 생각한다 (나는 C # 개발자 btw가 아니므로 코드에 대해 실례한다.)하지만 효율성의 관점에서 이것은 반드시해야합니까? 배열 인덱스로 열거 형을 사용하는 것은 유효한 C #입니다.


답변

플래그를 사용할 수 없거나 사용하지 않으려면 꼬리 재귀 함수를 사용하십시오. 64 비트 릴리스 모드에서 컴파일러는 goto명령문 과 매우 유사한 코드를 생성 합니다. 당신은 그것을 처리 할 필요가 없습니다.

int ComputeExampleValue(int i, ExampleEnum exampleValue) {
    switch (exampleValue) {
        case One: return i + 1;
        case OneAndTwo: return ComputeExampleValue(i + 2, ExampleEnum.One);
        case OneAndThree: return ComputeExampleValue(i + 3, ExampleEnum.One);
        case Two: return i + 2;
        case TwoAndThree: return ComputeExampleValue(i + 2, ExampleEnum.Three);
        case Three: return i + 3;
   }
}

답변

허용되는 솔루션은 문제가되지 않으며 문제에 대한 구체적인 솔루션입니다. 그러나 더 추상적 인 대안을 찾고 싶습니다.

내 경험상 논리 흐름을 정의하기 위해 열거 형을 사용하면 코드 디자인이 좋지 않은 경우가 종종 있습니다.

작년에 작업 한 코드에서 이런 일이 발생하는 실제 사례에 부딪 쳤습니다. 원래 개발자는 가져 오기 및 내보내기 논리를 모두 수행하고 열거 형을 기반으로 두 클래스 사이를 전환하는 단일 클래스를 만들었습니다. 이제 코드는 비슷하고 일부 중복 코드가 있었지만 코드를 읽기 어렵고 테스트하기가 거의 불가능 해졌습니다. 나는 그것을 두 개의 개별 클래스로 리팩토링하여 결국 모두 단순화하고 실제로보고되지 않은 많은 버그를 발견하고 제거 할 수있었습니다.

다시 한 번, 열거 형을 사용하여 논리 흐름을 제어하는 ​​것은 종종 디자인 문제라고 언급해야합니다. 일반적으로 Enum은 가능한 값이 명확하게 정의 된 형식에 안전하고 소비자에게 친숙한 값을 제공하는 데 주로 사용해야합니다. 논리 제어 메커니즘보다 속성 (예 : 테이블의 열 ID)으로 더 잘 사용됩니다.

질문에 제시된 문제를 고려해 봅시다. 나는 여기의 맥락이 나이 열거가 무엇을 나타내는 지 정말로 모른다. 그림 카드인가요? 그림을 그리기? 피를 뽑아? 순서가 중요합니까? 또한 성능이 얼마나 중요한지 모르겠습니다. 성능이나 메모리가 중요한 경우이 솔루션은 원하는 솔루션이 아닐 수 있습니다.

어쨌든 열거 형을 고려하십시오.

// All possible combinations of One - Eight.
public enum ExampleEnum {
    One,
    Two,
    TwoOne,
    Three,
    ThreeOne,
    ThreeTwo,
    ThreeOneTwo
}

우리가 여기에 가지고있는 것은 다른 비즈니스 개념을 나타내는 많은 다른 열거 형 값입니다.

대신 사용할 수있는 것은 사물을 단순화하는 추상화입니다.

다음 인터페이스를 고려해 봅시다.

public interface IExample
{
  void Draw();
}

그런 다음 이것을 추상 클래스로 구현할 수 있습니다.

public abstract class ExampleClassBase : IExample
{
  public abstract void Draw();
  // other common functionality defined here
}

우리는 그림 1, 2, 3 (논쟁을 위해 다른 논리를 가짐)을 나타내는 구체적인 클래스를 가질 수 있습니다. 이것들은 위에서 정의한 기본 클래스를 잠재적으로 사용할 수 있지만 DrawOne 개념이 열거 형으로 표현 된 개념과 다르다고 가정합니다.

public class DrawOne
{
  public void Draw()
  {
    // Drawing logic here
  }
}

public class DrawTwo
{
  public void Draw()
  {
    // Drawing two logic here
  }
}

public class DrawThree
{
  public void Draw()
  {
    // Drawing three logic here
  }
}

그리고 이제 우리는 다른 클래스에 대한 논리를 제공하기 위해 구성 될 수있는 세 개의 개별 클래스가 있습니다.

public class One : ExampleClassBase
{
  private DrawOne drawOne;

  public One(DrawOne drawOne)
  {
    this.drawOne = drawOne;
  }

  public void Draw()
  {
    this.drawOne.Draw();
  }
}

public class TwoOne : ExampleClassBase
{
  private DrawOne drawOne;
  private DrawTwo drawTwo;

  public One(DrawOne drawOne, DrawTwo drawTwo)
  {
    this.drawOne = drawOne;
    this.drawTwo = drawTwo;
  }

  public void Draw()
  {
    this.drawOne.Draw();
    this.drawTwo.Draw();
  }
}

// the other six classes here

이 접근법은 훨씬 더 장황합니다. 그러나 장점이 있습니다.

버그가 포함 된 다음 클래스를 고려하십시오.

public class ThreeTwoOne : ExampleClassBase
{
  private DrawOne drawOne;
  private DrawTwo drawTwo;
  private DrawThree drawThree;

  public One(DrawOne drawOne, DrawTwo drawTwo, DrawThree drawThree)
  {
    this.drawOne = drawOne;
    this.drawTwo = drawTwo;
    this.drawThree = drawThree;
  }

  public void Draw()
  {
    this.drawOne.Draw();
    this.drawTwo.Draw();
  }
}

누락 된 drawThree.Draw () 호출을 발견하는 것이 얼마나 간단합니까? 그리고 순서가 중요하다면, 드로우 콜의 순서도보고 따라 가기가 매우 쉽습니다.

이 접근법의 단점 :

  • 제시된 8 가지 옵션 중 하나는 모두 별도의 수업이 필요합니다
  • 이것은 더 많은 메모리를 사용합니다
  • 이렇게하면 코드가 표면적으로 커집니다
  • 때때로이 방법은 불가능하지만 약간의 변형이있을 수 있습니다.

이 방법의 장점 :

  • 이러한 각 클래스는 완전히 테스트 가능합니다. 때문에
  • Draw 메소드의 순환 복잡성은 낮습니다 (그리고 이론적으로 DrawOne, DrawTwo 또는 DrawThree 클래스를 조롱 할 수 있습니다)
  • 그리기 방법은 이해할 만합니다-개발자는 방법으로 수행하는 작업을 매듭으로 묶을 필요가 없습니다.
  • 버그는 찾기 쉽고 쓰기가 어렵습니다.
  • 클래스는보다 높은 수준의 클래스로 구성되므로 ThreeThreeThree 클래스를 정의하기가 쉽습니다.

case 문에 복잡한 논리 제어 코드를 작성해야 할 필요가있을 때마다이 (또는 유사한) 접근 방법을 고려하십시오. 미래에 당신은 당신이 기뻐할 것입니다.