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