[Proposal]: Allow for 'when' patterns on function parameters. #4182
-
Allow for 'when' patterns on function parameters.
SummaryHaving patterns on function parameters would allow for functions to be split in several independent blocks of code. ExampleA contrived example for clarity: public string Report(int number) when number < 0
{
return "I'm sorry you're in debt.";
}
public string Report(int number)
{
return "All clear.";
} The compiler could generate the following code with a dispatcher for this // dispatcher
public string Report(int number)
{
if (number < 0) return Report_@1(number);
return Report_@2(number);
}
internal string Report_@1(int number)
{
return "I'm sorry you're in debt.";
}
internal string Report_@2(int number)
{
return "All clear.";
} MotivationThis would allow for the following scenarios:
For developers it could mean:
For the .Net runtime it could mean faster code, for example DrawbacksOne could consider this feature as 'helping the compiler'. In general developers should not help the compiler, it's smarter than most of them. AlternativesDon't implement this and just use existing code patterns. Unresolved questionsWould this be limited to patterns or would expression also be allowed? The compiler would have an easier job proving patterns than expressions. On the other hand constant expressions calling constant functions can also be proven during compile time. Use the Would this be allowed for CompatibilityAs the generated dispatcher function is backward compatible with existing C# this feature would not break existing code bases. More examplesclass X : IEquatable<X>
{
public string Y {get; private set;}
public bool Equals(X? other) when other is null => false;
public bool Equals(X other) => Y.Equals(other.Y); // Can we leave out the question mark after X, or would that change the signature?
public void UpdateY(string? value) when value is null
{
throw new ArgumentNullException(nameof(value));
}
public void UpdateY(string value) when value is { Length < 3}
{
throw new ArgumentException("Too short", nameof(value));
}
public void UpdateY(string value) => y = value;
void Demo(object param) when param is SqlConnection
{
//...
}
void Demo(object param) when param is SqlTransaction
{
//...
}
} |
Beta Was this translation helpful? Give feedback.
Replies: 8 comments 4 replies
-
Hmm, I did search for existing proposals. I must have used the wrong searchterms. I think my 'Motivation' and suggested implementation' is better than in #1548 and #1468. The discussion boils down to the major argument that you can do the same in less code with switch statements or switch expressions. And I won't contest that, but I would like to know if my motivation for this proposal makes any sense and merits reconsideration? |
Beta Was this translation helpful? Give feedback.
-
I think the problems identified in those other discussions still hold. Overload resolution is entirely a compile-time concern in C#. The compiler determines which overload to call based on binding the types of the arguments and various rules about how they can be coerced or implicitly converted. A failure to match is caught at compile time. With this proposal (and the others) overload resolution has to be moved, at least in part, to runtime, via this "dispatcher". There is no longer a 1:1 mapping to the number of methods written to the number of methods emitted. Their lexical order in suddenly matters as it establishes the priority of the patterns, which doesn't really work well across As for the argument that it's less code, I don't see that. Considering that you have to repeat the signature each time for each of these "overloads" you're easily writing more code than if you had a series of |
Beta Was this translation helpful? Give feedback.
-
I'll close this issue.
|
Beta Was this translation helpful? Give feedback.
-
You could potentially use a source generator to add the dispatcher for you, based on an attribute? [Dispatcher("number < 0")]
internal string Report_@1(int number)
{
return "I'm sorry you're in debt.";
}
[Dispatcher] // no parameter
internal string Report_@2(int number)
{
return "All clear.";
} |
Beta Was this translation helpful? Give feedback.
-
The CLR only permits overloads with different signatures based on the arity and type of the parameters. Patterns couldn't be a part of that signature, not without some kind of runtime changes to accompany this feature. The compiler would still be required to emit a single copy of that method with the dispatch code written within it, and there wouldn't be another method that consumers could call directly to skip that dispatch. |
Beta Was this translation helpful? Give feedback.
-
I think we have a misunderstanding about what I'm proposing. I can see it clearly in my mind, but I don't think I got the idea across very well. In the example at the beginning, the programmer specifies 2 functions. The C# compiler emits 3 functions. One that I call the 'dispatcher' and thats gets called always if the compiler cannot not make a compile-time decision and the 2 original functions with a new unique name. Use case 1: const x = -1;
Report(x); In this case the C# compiler can prove based on the pattern which function to call and emits a call to Use case 2: void Demo(int x)
{
Report(x);
} In this the compiler can most likely not prove which particular version to call and emits a call to So I do not see where run-time changes would be necessary as I propose this as a compile time feature and after the C# compiler is done with it, only classic backward compatible code remains. If this gets implemented and used this contrived example code could skip 2 null checks. And the generated dispatcher function would not be slower than the current version of string.Equals. string a = 'test';
string b = 'Test';
if (string.Equals(a,b))
{ ... } I know these are micro optimisations and null checks and most compares are very fast, but every CPU cycle saved is a CPU cycle saved. As for the order of patterns that remains an issue, so I was thinking:
So for example the example string.Equals you could have the patterns: Equals(string a, string b) when a is null and b is null => true;
Equals(string a, string b) when a is null and b is not null => false;
Equals(string a, string b) when a is not null and b is null => false;
Equals(string a, string b) when a is not null and b is not null { ... } And yes that can add up to a lot of typing code, but if it saves me a few milliseconds of execution time, I don't mind. |
Beta Was this translation helpful? Give feedback.
-
My first test:I made a copy from the reference source of Than I made a 'dispatcher' version of it: public static TSource FirstOrDefault<TSource>(this IEnumerable<TSource> source)
{
if (source is null) return FirstOrDefault_0(source); // pattern: source is null
if (source is IList<TSource>) return FirstOrDefault_1(source); // pattern : source is IList<TSource>
// no pattern
using (IEnumerator<TSource> e = source.GetEnumerator())
{
if (e.MoveNext()) return e.Current;
}
return default(TSource);
}
public static TSource FirstOrDefault_0<TSource>(this IEnumerable<TSource> source)
{
throw Error.ArgumentNull("source");
}
public static TSource FirstOrDefault_1<TSource>(this IEnumerable<TSource> source)
{
var list = source as IList<TSource>;
if (list.Count > 0) return list[0];
return default;
} There are the results:
Observations
What are other good candidates to test this proposal with? |
Beta Was this translation helpful? Give feedback.
See:
#1548
#1468
dotnet/roslyn#955