C
C#15mo ago
__dil__

✅ Switch pattern matching with integers

Is this a valid/idiomatic way to use pattern matching to compare integers?
public static double SuccessRate(int speed) =>
speed switch
{
0 => 0.0,
< 5 => 1.0,
< 9 => 0.9,
9 => 0.8,
10 => 0.77,
_ => throw new Exception($"Expected speed between 0 and 10, got `{speed}`")
};
public static double SuccessRate(int speed) =>
speed switch
{
0 => 0.0,
< 5 => 1.0,
< 9 => 0.9,
9 => 0.8,
10 => 0.77,
_ => throw new Exception($"Expected speed between 0 and 10, got `{speed}`")
};
One thing bugging me in particular is: are the arms checked in the order they were written, or is the order not guaranteed? If the order is not guaranteed, then obviously the code above does not work.
29 Replies
Angius
Angius15mo ago
It is valid As for the order... $tias
Angius
Angius15mo ago
But I believe it does work in order
__dil__
__dil__OP15mo ago
I mean, it could work even if the order wasn't guaranteed so it's not really a "try it and see" kind of deal the compiler could prefer the textual order but reserve the right to rearrange if it optimizes better it's something that should be mentioned in the docs/spec, but I can't really find anything :/
cap5lut
cap5lut15mo ago
also if thats real code and not an example, negative numbers will always go into the < 5 branch thus result in 1.0
__dil__
__dil__OP15mo ago
I think you're incorrect, as the official docs do state this:
The result of a switch expression is the value of the expression of the first switch expression arm whose pattern matches the input expression and whose case guard, if present, evaluates to true. The switch expression arms are evaluated in text order.
In any case, the fact the compiler transforms the code doesn't matter insofar the transformed code respects the established contract (i.e. that the arms are evaluated in textual order). Fair enough! I personally would've used an unsigned number here as negative speed doesn't make sense in this situation, but this is just example code from The Internet(TM), so yeah
Thinker
Thinker15mo ago
The compiler doesn't even allow you to define a pattern if all possible values of that pattern have already been handled by previous arms.
MODiX
MODiX15mo ago
Thinker
REPL Result: Failure
string Foo(int x) => x switch
{
0 => "a",
> 0 => "b",
5 => "c",
};
string Foo(int x) => x switch
{
0 => "a",
> 0 => "b",
5 => "c",
};
Exception: CompilationErrorException
- The pattern is unreachable. It has already been handled by a previous arm of the switch expression or it is impossible to match.
- The pattern is unreachable. It has already been handled by a previous arm of the switch expression or it is impossible to match.
Compile: 456.687ms | Execution: 0.000ms | React with ❌ to remove this embed.
Thinker
Thinker15mo ago
So yes, it's effectively "in order"
__dil__
__dil__OP15mo ago
Just to be clear, what you mentioned previously (compiler prevents overlapping patterns) isn't a direct answer. You can have all disjoint patterns where having guaranteed order still matters. For example, if you know there is a case that happens much more frequently, then knowing the the order is guaranteed makes it possible to put that arm first to get performance benefits.
Thinker
Thinker15mo ago
I guess? What kinds of objects are you working with where this would matter? I think the compiler tries to optimize patterns as much as possible
__dil__
__dil__OP15mo ago
I'm not saying it does always matter, just that it's a consideration, at least theoretically. the compiler can't know the runtime patterns of my application though, so that's a choice I have to make as a dev.
Thinker
Thinker15mo ago
Again, what objects do you have where this would matter?
__dil__
__dil__OP15mo ago
The worst-case scenario is one where you have a lot of arms, and you unknowingly put the case that is by far the most common as the very last arm that means, if I understand correctly, that the code would perform tons of conditional checks before finally getting to the last arm I'm not saying that it does always matter, I'm just saying it can be a concern you have to understand that I come from a systems programming background, so these are the kind of issues I care about 🙂
MODiX
MODiX15mo ago
Thinker
sharplab.io (click here)
object x = 0;
string s = x switch {
string => "string",
int => "int",
bool => "bool",
};
object x = 0;
string s = x switch {
string => "string",
int => "int",
bool => "bool",
};
React with ❌ to remove this embed.
Thinker
Thinker15mo ago
Seems like order does matter
cap5lut
cap5lut15mo ago
what i meant is that the order can be different to some degree as long as it doesnt change the behavior eg, the transformed code checks first for < 5 and then for == 0
JakenVeina
JakenVeina15mo ago
the compiler emits the equivalent of if/else blocks, in the order you listed, but that could easily get optimized into a different order by the JIT or by the CPU
reflectronic
reflectronic15mo ago
the C# compiler will absolutely screw with the order it is not guaranteed to be the same order as the one written in the code, even for simple cases
reflectronic
reflectronic15mo ago
they are unclear. it will never mess with the order in a way that causes visible side-effects. (for this purpose, side-effects are when clauses. they are not property accesses or Deconstruct method calls, which may still be reordered, but are expected to behave well) but it is pretty easy to see that, in cases without side effects, it absolutely doesn't just replicate the order of the code. take this example:
public int M(int i)
{
return i switch
{
< 10 => 1,
< 100 => 2,
< 1000 => 3,
_ => 0,
};
}
public int M(int i)
{
return i switch
{
< 10 => 1,
< 100 => 2,
< 1000 => 3,
_ => 0,
};
}
JakenVeina
JakenVeina15mo ago
it's the JIT/AOT compiler that's gonna do that, though, not Roslyn
reflectronic
reflectronic15mo ago
not neccessarily! the generated IL code for the above example looks like (after decompiling)
public int M(int i)
{
if (i < 100)
{
if (i < 10)
{
return 1;
}
return 2;
}
if (i < 1000)
{
return 3;
}
return 0;
}
public int M(int i)
{
if (i < 100)
{
if (i < 10)
{
return 1;
}
return 2;
}
if (i < 1000)
{
return 3;
}
return 0;
}
you can see that Roslyn turned this into a binary search. this does the comparisons in a different order from the original source in general, it will rearrange the order of the comparisons so as to minimize the number of comparisons required in the worst-case. or, to be more specific, the set of comparisons in the switch is turned into a DAG, the DAG is topologically sorted, and that is used as the order of comparisons. (this is not a guarantee, merely the current implementation.) which addresses this concern in a way, though it makes it harder to manually "tune"
JakenVeina
JakenVeina15mo ago
hmmmmm
reflectronic
reflectronic15mo ago
if you know that 0-9 is the overwhelmingly common case for this method, and you want to minimize the number of comparisons in that case, you would have to write the ifs manually. sorry. or, you can trust the compiler, and allow PGO to clean it up. that would indeed be the work of the JIT compiler
reflectronic
reflectronic15mo ago
actually PGO can't clean it up yet. https://github.com/dotnet/runtime/issues/75733 i suppose they take PRs. :)
GitHub
PGO: Re-order blocks without side-effects · Issue #75733 · dotnet/r...
Consider the following example: static void Main(string[] args) { Console.WriteLine((int)'3'); for (int i = 0; i < 100; i++) { foo('3'); Thread.Sleep(16); } } [MethodImpl(MethodI...
JakenVeina
JakenVeina15mo ago
reordering of blocks without side-effects is something STephen talked about coming in .NET 8, wasn't it?
__dil__
__dil__OP15mo ago
Thanks for the insights and the discussions @jakenveina @reflectronic @angius, I appreciate it.
Want results from more Discord servers?
Add your server