C # 스위치 명령문 제한-왜?
switch 문을 작성할 때 case 문에서 켤 수있는 항목에는 두 가지 제한이있는 것으로 보입니다.
예를 들어 (그렇습니다, 당신이 이런 종류의 일을하고 있다면 그것은 아마도 객체 지향 (OO) 아키텍처가 iffy라는 것을 의미합니다 -이것은 단지 고안된 예입니다!)
Type t = typeof(int);
switch (t) {
case typeof(int):
Console.WriteLine("int!");
break;
case typeof(string):
Console.WriteLine("string!");
break;
default:
Console.WriteLine("unknown!");
break;
}
여기서 switch () 문은 '정수 유형의 예상 값'으로 실패하고 사례 문은 '상수 값이 예상됩니다'로 실패합니다.
이러한 제한이 필요한 이유는 무엇이며 기본 정당성은 무엇입니까? switch 문 이 정적 분석에만 성공해야하는 이유와 켜져있는 값이 완전한 (즉, 기본) 이유를 알 수 없습니다. 정당화 란 무엇입니까?
이것은 내 원래 게시물이며, 토론이 시작되었습니다 ... 왜냐하면 잘못 되었기 때문입니다 .
switch 문은 큰 if-else 문과 동일하지 않습니다. 각 사례는 고유해야하며 정적으로 평가되어야합니다. switch 문은 보유한 사례 수에 관계없이 일정한 시간 분기를 수행합니다. if-else 문은 각 조건이 참일 때까지 각 조건을 평가합니다.
실제로 C # switch 문은 항상 일정한 시간 분기 가 아닙니다 .
어떤 경우에는 컴파일러가 CIL switch 문을 사용하는데, 이는 실제로 점프 테이블을 사용하는 일정한 시간 분기입니다. 그러나 Ivan Hamilton 이 지적한 드문 경우 에는 컴파일러가 완전히 다른 것을 생성 할 수 있습니다.
이것은 다양한 C # 스위치 문, 일부 희박하고 밀도가 높고 ildasm.exe 도구를 사용하여 결과 CIL을 보면 실제로 쉽게 확인할 수 있습니다.
C # switch 문을 CIL switch 명령어와 혼동하지 않는 것이 중요합니다.
CIL 스위치는 점프 테이블이므로 점프 주소 세트에 대한 색인이 필요합니다.
이것은 C # 스위치의 케이스가 인접한 경우에만 유용합니다.
case 3: blah; break;
case 4: blah; break;
case 5: blah; break;
그러나 그렇지 않은 경우 거의 사용하지 않습니다.
case 10: blah; break;
case 200: blah; break;
case 3000: blah; break;
(3 개의 슬롯 만 사용하여 ~ 3000 개의 테이블 크기 항목이 필요합니다)
인접하지 않은 표현식을 사용하면 컴파일러는 선형 if-else-if-else 검사를 수행하기 시작할 수 있습니다.
인접하지 않은 더 큰 표현식 집합을 사용하면 컴파일러는 이진 트리 검색으로 시작하여 마지막 몇 개의 항목 인 경우 if-else-if-else로 시작할 수 있습니다.
인접한 항목의 덩어리를 포함하는 표현식 세트를 사용하면 컴파일러는 이진 트리 검색과 마지막으로 CIL 스위치를 사용할 수 있습니다.
이것은 "mays"& "mights"로 가득 차 있으며 컴파일러에 따라 다릅니다 (Mono 또는 Rotor와 다를 수 있음).
인접한 경우를 사용하여 결과를 컴퓨터에 복제했습니다.
10 웨이 스위치를 실행하는 총 시간, 10000 회 반복 (ms) : 25.1383
10 웨이 스위치 당 대략적인 시간 (ms) : 0.0025138350 웨이 스위치를 실행하는 총 시간, 10000 회 반복 (ms) : 26.593
50 웨이 스위치 당 대략적인 시간 (ms) : 0.00265935000 웨이 스위치를 실행하는 총 시간, 10000 반복 (ms) : 23.7094
5000 웨이 스위치 (ms) 당 대략적인 시간 : 0.0023709450000 웨이 스위치를 실행하는 총 시간, 10000 반복 (ms) : 205000 년
스위치 (ms) 당 대략적인 시간 : 0.00200933
그런 다음 인접하지 않은 케이스 표현식을 사용했습니다.
10 웨이 스위치를 실행하는 총 시간, 10000 회 반복 (ms) : 19.6189
10 웨이 스위치 당 대략적인 시간 (ms) : 0.00196189500 웨이 스위치를 실행하는 총 시간, 10000 반복 (ms) : 19.1664
500 웨이 스위치 (ms) 당 대략적인 시간 : 0.001916645000 웨이 스위치를 실행하는 총 시간, 10000 반복 (ms) : 19,5871
5000 웨이 스위치 (ms) 당 대략적인 시간 : 0.00195871인접하지 않은 50,000 개의 case switch 문은 컴파일되지 않습니다.
"표현식이 너무 길거나 복잡하여 'ConsoleApplication1.Program.Main (string [])'근처에서 컴파일 할 수 없습니다.
여기서 재미있는 점은 이진 트리 검색이 CIL 스위치 명령보다 약간 빠르다는 것입니다.
Brian, " constant " 라는 단어를 사용했는데 , 이는 계산 복잡도 이론 관점에서 매우 명확한 의미를 갖습니다. 단순한 인접 정수 예제는 O (1) (일정)로 간주되는 CIL을 생성 할 수 있지만, 희소 예제는 O (log n) (대수)이며 군집 된 예제는 중간에 있고 작은 예제는 O (n)입니다 (선형) ).
이것은 정적을 Generic.Dictionary<string,int32>
만들 수있는 String 상황조차 다루지 않으며 처음 사용할 때 명확한 오버 헤드를 겪게됩니다. 여기서의 성능은의 성능에 따라 다릅니다 Generic.Dictionary
.
CIL 사양이 아닌 C # 언어 사양 을 확인하면 "15.7.2 switch statement"에 "일정한 시간"에 대한 언급이 없거나 기본 구현에서 CIL 스위치 명령어를 사용하기도합니다 (매우 신중하게 가정해야 함). 그런 것들).
하루가 끝나면 현대 시스템의 정수 식에 대한 C # 스위치는 마이크로 초 미만의 작업이며 일반적으로 걱정할 가치가 없습니다.
물론이 시간은 기계와 조건에 따라 다릅니다. 이 타이밍 테스트에주의를 기울이지 않을 것입니다. 우리가 말하는 마이크로 초의 지속 시간은 실행중인 "실제"코드로 인해 줄어 듭니다 (그리고 "실제 코드"를 포함해야합니다. 그렇지 않으면 컴파일러가 분기를 최적화합니다). 시스템의 지터. 내 대답은 IL DASM 을 사용 하여 C # 컴파일러로 만든 CIL을 검사 한 결과입니다. 물론 CPU가 실행하는 실제 명령이 JIT에 의해 생성되므로 최종적인 것은 아닙니다.
x86 컴퓨터에서 실제로 실행되는 최종 CPU 명령어를 확인했으며 다음과 같은 간단한 인접 세트 스위치를 확인할 수 있습니다.
jmp ds:300025F0[eax*4]
이진 트리 검색으로 가득 찬 곳 :
cmp ebx, 79Eh
jg 3000352B
cmp ebx, 654h
jg 300032BB
…
cmp ebx, 0F82h
jz 30005EEE
가장 먼저 떠오르는 이유는 역사적입니다 .
대부분의 C, C ++ 및 Java 프로그래머는 그러한 자유에 익숙하지 않기 때문에 요구하지 않습니다.
또 다른 더 유효한 이유는 언어 복잡성이 증가 한다는 것입니다 .
우선, 객체 .Equals()
와 ==
연산자를 비교해야 합니까? 두 경우 모두 유효합니다. 이를 위해 새로운 구문을 도입해야합니까? 프로그래머가 자체 비교 방법을 소개하도록 허용해야합니까?
또한 객체를 켤 수있게 하면 switch 문에 대한 기본 가정 이 깨 집니다. 개체를 켤 수있는 경우 컴파일러에서 적용 할 수없는 switch 문에 적용되는 두 가지 규칙이 있습니다 ( C # 버전 3.0 언어 사양 §8.7.2 참조).
- 스위치 라벨의 값이 일정하다는 것을
- 스위치 레이블의 값이 고유해야합니다 (따라서 주어진 스위치 표현에 대해 하나의 스위치 블록 만 선택할 수 있습니다)
상수가 아닌 경우 값이 허용되었다는 가설의 경우이 코드 예제를 고려하십시오.
void DoIt()
{
String foo = "bar";
Switch(foo, foo);
}
void Switch(String val1, String val2)
{
switch ("bar")
{
// The compiler will not know that val1 and val2 are not distinct
case val1:
// Is this case block selected?
break;
case val2:
// Or this one?
break;
case "bar":
// Or perhaps this one?
break;
}
}
코드는 무엇을합니까? 사례 진술이 재정렬되면 어떻게됩니까? 실제로 C #이 스위치를 넘어 뜨린 이유 중 하나는 스위치 문을 임의로 재 배열 할 수 있기 때문입니다.
이러한 규칙은 이유가 있으므로 프로그래머는 하나의 사례 블록을보고 블록이 입력되는 정확한 조건을 알 수 있습니다. 위에서 언급 한 스위치 문이 100 줄 이상으로 커지면 그러한 지식은 매우 중요합니다.
By the way, VB, having the same underlying architecture, allows much more flexible Select Case
statements (the above code would work in VB) and still produces efficient code where this is possible so the argument by techical constraint has to be considered carefully.
Mostly, those restrictions are in place because of language designers. The underlying justification may be compatibility with languange history, ideals, or simplification of compiler design.
The compiler may (and does) choose to:
- create a big if-else statement
- use a MSIL switch instruction (jump table)
- build a Generic.Dictionary<string,int32>, populate it on first use, and call Generic.Dictionary<>::TryGetValue() for a index to pass to a MSIL switch instruction (jump table)
- use a combination of if-elses & MSIL "switch" jumps
The switch statement IS NOT a constant time branch. The compiler may find short-cuts (using hash buckets, etc), but more complicated cases will generate more complicated MSIL code with some cases branching out earlier than others.
To handle the String case, the compiler will end up (at some point) using a.Equals(b) (and possibly a.GetHashCode() ). I think it would be trival for the compiler to use any object that satisfies these constraints.
As for the need for static case expressions... some of those optimisations (hashing, caching, etc) would not be available if the case expressions weren't deterministic. But we've already seen that sometimes the compiler just picks the simplistic if-else-if-else road anyway...
Edit: lomaxx - Your understanding of the "typeof" operator is not correct. The "typeof" operator is used to obtain the System.Type object for a type (nothing to do with its supertypes or interfaces). Checking run-time compatibility of an object with a given type is the "is" operator's job. The use of "typeof" here to express an object is irrelevant.
While on the topic, according to Jeff Atwood, the switch statement is a programming atrocity. Use them sparingly.
You can often accomplish the same task using a table. For example:
var table = new Dictionary<Type, string>()
{
{ typeof(int), "it's an int!" }
{ typeof(string), "it's a string!" }
};
Type someType = typeof(int);
Console.WriteLine(table[someType]);
I don't see any reason why the switch statement has to succomb to static analysis only
True, it doesn't have to, and many languages do in fact use dynamic switch statements. This means however that reordering the "case" clauses can change the behaviour of the code.
There's some interesting info behind the design decisions that went into "switch" in here: Why is the C# switch statement designed to not allow fall-through, but still require a break?
Allowing dynamic case expressions can lead to monstrosities such as this PHP code:
switch (true) {
case a == 5:
...
break;
case b == 10:
...
break;
}
which frankly should just use the if-else
statement.
Microsoft finally heard you!
Now with C# 7 you can:
switch(shape)
{
case Circle c:
WriteLine($"circle with radius {c.Radius}");
break;
case Rectangle s when (s.Length == s.Height):
WriteLine($"{s.Length} x {s.Height} square");
break;
case Rectangle r:
WriteLine($"{r.Length} x {r.Height} rectangle");
break;
default:
WriteLine("<unknown shape>");
break;
case null:
throw new ArgumentNullException(nameof(shape));
}
This is not a reason why, but the C# specification section 8.7.2 states the following:
The governing type of a switch statement is established by the switch expression. If the type of the switch expression is sbyte, byte, short, ushort, int, uint, long, ulong, char, string, or an enum-type, then that is the governing type of the switch statement. Otherwise, exactly one user-defined implicit conversion (§6.4) must exist from the type of the switch expression to one of the following possible governing types: sbyte, byte, short, ushort, int, uint, long, ulong, char, string. If no such implicit conversion exists, or if more than one such implicit conversion exists, a compile-time error occurs.
The C# 3.0 specification is located at: http://download.microsoft.com/download/3/8/8/388e7205-bc10-4226-b2a8-75351c669b09/CSharp%20Language%20Specification.doc
Judah's answer above gave me an idea. You can "fake" the OP's switch behavior above using a Dictionary<Type, Func<T>
:
Dictionary<Type, Func<object, string, string>> typeTable = new Dictionary<Type, Func<object, string, string>>();
typeTable.Add(typeof(int), (o, s) =>
{
return string.Format("{0}: {1}", s, o.ToString());
});
This allows you to associate behavior with a type in the same style as the switch statement. I believe it has the added benefit of being keyed instead of a switch-style jump table when compiled to IL.
I suppose there is no fundamental reason why the compiler couldn't automatically translate your switch statement into:
if (t == typeof(int))
{
...
}
elseif (t == typeof(string))
{
...
}
...
But there isn't much gained by that.
A case statement on integral types allows the compiler to make a number of optimizations:
There is no duplication (unless you duplicate case labels, which the compiler detects). In your example t could match multiple types due to inheritance. Should the first match be executed? All of them?
The compiler can choose to implement a switch statement over an integral type by a jump table to avoid all the comparisons. If you are switching on an enumeration that has integer values 0 to 100 then it creates an array with 100 pointers in it, one for each switch statement. At runtime it simply looks up the address from the array based on the integer value being switched on. This makes for much better runtime performance than performing 100 comparisons.
According to the switch statement documentation if there is an unambiguous way to implicitly convert the the object to an integral type, then it will be allowed. I think you are expecting a behavior where for each case statement it would be replaced with if (t == typeof(int))
, but that would open a whole can of worms when you get to overload that operator. The behavior would change when implementation details for the switch statement changed if you wrote your == override incorrectly. By reducing the comparisons to integral types and string and those things that can be reduced to integral types (and are intended to) they avoid potential issues.
wrote:
"The switch statement does a constant time branch regardless of how many cases you have."
Since the language allows the string type to be used in a switch statement I presume the compiler is unable to generate code for a constant time branch implementation for this type and needs to generate an if-then style.
@mweerden - Ah I see. Thanks.
I do not have a lot of experience in C# and .NET but it seems the language designers do not allow static access to the type system except in narrow circumstances. The typeof keyword returns an object so this is accessible at run-time only.
I think Henk nailed it with the "no sttatic access to the type system" thing
Another option is that there is no order to types where as numerics and strings can be. Thus a type switch would can't build a binary search tree, just a linear search.
I agree with this comment that using a table driven approach is often better.
In C# 1.0 this was not possible because it didn't have generics and anonymous delegates. New versions of C# have the scaffolding to make this work. Having a notation for object literals is also helps.
I have virtually no knowledge of C#, but I suspect that either switch was simply taken as it occurs in other languages without thinking about making it more general or the developer decided that extending it was not worth it.
Strictly speaking you are absolutely right that there is no reason to put these restrictions on it. One might suspect that the reason is that for the allowed cases the implementation is very efficient (as suggested by Brian Ensink (44921)), but I doubt the implementation is very efficient (w.r.t. if-statements) if I use integers and some random cases (e.g. 345, -4574 and 1234203). And in any case, what is the harm in allowing it for everything (or at least more) and saying that it is only efficient for specific cases (such as (almost) consecutive numbers).
I can, however, imagine that one might want to exclude types because of reasons such as the one given by lomaxx (44918).
Edit: @Henk (44970): If Strings are maximally shared, strings with equal content will be pointers to the same memory location as well. Then, if you can make sure that the strings used in the cases are stored consecutively in memory, you can very efficiently implement the switch (i.e. with execution in the order of 2 compares, an addition and two jumps).
참고URL : https://stackoverflow.com/questions/44905/c-sharp-switch-statement-limitations-why
'Programing' 카테고리의 다른 글
지정된 문자열로 시작하는 파일 이름을 가진 모든 파일을 찾으십니까? (0) | 2020.06.24 |
---|---|
TypeScript에서 숫자를 문자열로 캐스팅 (0) | 2020.06.24 |
포스트 백이란 무엇입니까? (0) | 2020.06.24 |
Spring MVC : @ResponseBody에서 이미지를 반환하는 방법은 무엇입니까? (0) | 2020.06.24 |
시프트 연산자는 Java에서 어떻게 작동합니까? (0) | 2020.06.24 |