I am exploring Blazor's QuickGrid source code and found one interesting spot here.
On the 45th line Steve Sanderson left a TODO with a potentially better alternative solution.
I could not resist my curiosity and decided to give it a try and benchmark the solution afterwards. But, unfortunately, my knowledge in Reflection is really poor.
Could anybody help me to understand how the ideas described in the Steve's comment could be achieved?
Thanks
UPD-1: Code snippet of what I have atm
if( typeof(TProp).GetInterface(nameof(IFormattable)) != null ) {
Type result = typeof(Func<,>).MakeGenericType(typeof(TGridItem), typeof(TProp));
_cellTextFunc = item => FormatValue(compiledPropertyExpression);
}
// TODO: Consider using reflection to avoid having to box every value just to call IFormattable.ToString
// For example, define a method "string Format<U>(Func<TGridItem, U> property) where U: IFormattable", and
// then construct the closed type here with U=TProp when we know TProp implements IFormattable
private string FormatValue<U>( Func<TGridItem, U> property ) where U : IFormattable
{
return null;
}
CodePudding user response:
I would go with a generic interface such as IFormatter<T>
without a generic constraint, and a few private implementations with the necessary complaint, and use reflection internally to decide which private implementation to use - a simple version for everything except Nullable<T>
, and a special-case for that; this is the same approach used by EqualityComparer<T>.Default
.
Full example is below, noting that Formatter<T>.Instance
will be null
if the T
doesn't support IFormattable
(to allow testing, although this could also be handled separately).
The use of generics in the private implementation means we're using "constrained call", so: no boxing there. The special-casing of Nullable<T>
means we can handle that too, with an additional indirection.
The important thing is that we only do the thinking once per type - testing whether the type is nullable, whether it implements IFormattable
, etc - and this is then cached via a strategy instance. If we do the thinking every time: boxing would probably be cheaper!
Relevant usage for the sample provided (note you could probably simply with a non-generic static class with generic method so you can just do Formatter.Format(value, format, provider)
and use generic inference from value
for the rest):
// L50
if (Formatter<TProp>.Instance is null)
{
throw new InvalidOperationException($"A '{nameof(Format)}' parameter was supplied, but the type '{typeof(TProp)}' does not implement '{typeof(IFormattable)}'.");
}
_cellTextFunc = item => Formatter<TProp>.Instance.Format(compiledPropertyExpression!(item), Format, null);
Console.WriteLine(Formatter<int>.Instance!.Format(123, "G"));
Console.WriteLine(Formatter<int?>.Instance!.Format(123, "G"));
Console.WriteLine(Formatter<int?>.Instance!.Format(null, "G"));
Console.WriteLine(Formatter<NotFormattable?>.Instance is null);
struct NotFormattable { }
public interface IFormatter<T>
{
string Format(T value, string? format, IFormatProvider? formatProvider = null);
}
public static class Formatter<T>
{
public static IFormatter<T>? Instance { get; }
static Formatter()
{
object? instance = null;
var underlying = Nullable.GetUnderlyingType(typeof(T));
if (typeof(IFormattable).IsAssignableFrom(underlying ?? typeof(T)))
{
if (typeof(T).IsValueType)
{
if (underlying is null)
{
instance = Activator.CreateInstance(typeof(SupportedValueTypeFormatter<>).MakeGenericType(typeof(T)));
}
else
{
instance = Activator.CreateInstance(typeof(SupportedNullableFormatter<>).MakeGenericType(underlying));
}
}
else
{
instance = Activator.CreateInstance(typeof(SupportedRefTypeFormatter<>).MakeGenericType(typeof(T)));
}
}
Instance = (IFormatter<T>?)instance;
}
}
internal sealed class SupportedNullableFormatter<T> : IFormatter<T?>
where T : struct, IFormattable
{
public string Format(T? value, string? format, IFormatProvider? formatProvider)
=> value is null ? "" : value.GetValueOrDefault().ToString(format, formatProvider);
}
internal sealed class SupportedValueTypeFormatter<T> : IFormatter<T>
where T : struct, IFormattable
{
public string Format(T value, string? format, IFormatProvider? formatProvider)
=> value.ToString(format, formatProvider);
}
internal sealed class SupportedRefTypeFormatter<T> : IFormatter<T>
where T : class, IFormattable
{
public string Format(T value, string? format, IFormatProvider? formatProvider)
=> value is null ? "" : value.ToString(format, formatProvider);
}
CodePudding user response:
I suspect what Steve meant was to define a method such as:
private string? FormatItem<U>(TGridItem item, Func<TGridItem, U> property) where U: IFormattable
{
return property(item)?.ToString(Format, null);
}
(I had to add item
to make it make sense: perhaps I'm missing something).
Then once you know U
, you can create a delegate which invokes this method:
formatItemMethodInfo = typeof(C<TGridItem, TProp>).GetMethod("FormatItem", BindingFlags.NonPublic | BindingFlags.Instance)!;
var formatter = (Func<TGridItem, Func<TGridItem, TProp>, string>)Delegate.CreateDelegate(
typeof(Func<TGridItem, Func<TGridItem, TProp>, string>),
this,
formatItemMethodInfo.MakeGenericMethod(typeof(TProp)));
And finally use this when constructing _cellTextFunc
:
_cellTextFunc = item => formatter(item, compiledPropertyExpression);
This has a little bit of upfront cost (creating the delegate), but invoking it is cheap, and means that you don't have to box item
to IFormattable
if it's a value type.