I basically want two separate overloads for string/FormattableString (the background is that I want to nudge people towards using string constants for log messages and pass parameters via structured logging instead of the log message to simplify analysis. So the FormattableString logging method would be obsoleted).
Now due to the way the compiler works, you cannot directly overload the methods, because a FormattableString devolves to a string before it's being passed. What does work though is to have a wrapper struct that defines implicit overloads:
public struct StringIfNotFormattableStringAdapter
{
public string StringValue { get; }
private StringIfNotFormattableStringAdapter(string s)
{
StringValue = s;
}
public static implicit operator StringIfNotFormattableStringAdapter(string s)
{
return new StringIfNotFormattableStringAdapter(s);
}
public static implicit operator StringIfNotFormattableStringAdapter(FormattableString fs)
{
throw new InvalidOperationException("This only exists to allow correct overload resolution. " +
"This should never be called since the FormattableString overload should be preferred to this.");
}
}
public static class Test
{
public static void Log(StringIfNotFormattableStringAdapter msg)
{
}
public static void Log(FormattableString msg)
{
}
public static void Foo()
{
Log("Hello"); // resolves to StringIfNotFormattableStringAdapter overload
Log($"Hello"); // resolves to FormattableString overload
}
}
So far so good.
What I don't understand: Why does removing the
implicit operator StringIfNotFormattableStringAdapter(FormattableString fs)
cause the call Log($"Hello")
to become ambiguous?
CS0121 The call is ambiguous between the following methods or properties: Test.Log(StringIfNotFormattableStringAdapter)' and 'Test.Log(FormattableString)'`
According to C# Specification, Interpolated strings, there is an implicit conversion from interpolated string to FormattableString
:
An interpolated_string_expression is classified as a value. If it is immediately converted to
System.IFormattable
orSystem.FormattableString
with an implicit interpolated string conversion (Implicit interpolated string conversions), the interpolated string expression has that type. Otherwise, it has the typestring
.
In the provided code there is also conversion of string
to StringIfNotFormattableStringAdapter
.
Method invocation
Log($"Hello");
can be resolved to both Log
methods, because interpolated string expression $"Hello"
can be:
FormattableString
as interpolated string;StringIfNotFormattableStringAdapter
as string
.Here an ambiquity occurs to compiler and it needs additional rules to resolve this ambiguity. To resolve the ambiguity compiler uses rules that are described in C# Specification, Better Conversion Target (go to the bottom of the page 164). The rules say that:
Given two different types
T1
andT2
,T1
is a better conversion target thanT2
if no implicit conversion fromT2
toT1
exists, and at least one of the following holds:
An implicit conversion from
T1
toT2
exists(other rules are not important for our case)
In the provided code FormattableString
is better conversion than StringIfNotFormattableStringAdapter
because
StringIfNotFormattableStringAdapter
to FormattableString
and
FormattableString
to StringIfNotFormattableStringAdapter
exists.Therefore compiler prefers to convert interpolated string $"Hello"
to FormattableString
and then invokes method Log(FormattableString)
.
Why does removing the
implicit operator StringIfNotFormattableStringAdapter(FormattableString fs)
cause the call
Log($"Hello")
to become ambiguous?
Because when you remove this operator the second rule ("an implicit conversion from FormattableString
to StringIfNotFormattableStringAdapter
exists") breaks and now compiler cannot define better conversion target. This causes an ambiguity to compiler and compilation error occurs.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With