Reputation: 266
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;
}
Upvotes: 2
Views: 362
Reputation: 42330
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).
We also need to define a spearate version for nullables, since Nullable<T>
can't satisfy the constraint IFormattable<T>
, even if T
does implement IFormattable<T>
.
private string? FormatItemNullable<U>(TGridItem item, Func<TGridItem, U?> property) where U : struct, IFormattable
{
return property(item)?.ToString(Format, null);
}
Then once you know U
, you can create a delegate which invokes this method:
var formatItemMethodInfo = typeof(C<TGridItem, TProp>).GetMethod("FormatItem", BindingFlags.NonPublic | BindingFlags.Instance)!;
var formatItemNullableMethodInfo = typeof(C<TGridItem, TProp>).GetMethod("FormatItemNullable", BindingFlags.NonPublic | BindingFlags.Instance)!;
var formatter = (Func<TGridItem, Func<TGridItem, TProp>, string>)Delegate.CreateDelegate(
typeof(Func<TGridItem, Func<TGridItem, TProp>, string>),
this,
nullableUnderlyingTypeOrNull == null
? formatItemMethodInfo.MakeGenericMethod(typeof(TProp))
: formatItemNullableMethodInfo.MakeGenericMethod(nullableUnderlyingTypeOrNull));
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.
If I were to suggest a better solution, since we're already using compiled expressions with Property
, I'd extend that and compile the ToString
call into the expression. Something like:
var toStringMethod = typeof(IFormattable).GetMethod("ToString")!;
var propVar = Expression.Variable(typeof(TProp), "prop");
// What to call String(...) on. We need to call prop.GetValueOrDefault().ToString(...)
// if prop is a nullable value type
Expression toStringCallTarget = nullableUnderlyingTypeOrNull == null
? propVar
: Expression.Call(
propVar,
typeof(TProp).GetMethod("GetValueOrDefault", Type.EmptyTypes)!);
// The ToString(...) call itself
var toStringCall = Expression.Call(
toStringCallTarget,
toStringMethod,
Expression.Property(Expression.Constant(this), "Format"),
Expression.Constant(null, typeof(IFormatProvider)));
// If prop is nullable (class or nullable struct), then we need to do
// prop == null ? null : prop[.GetValueOrDefault()].ToString(...),
// otherwise just call prop.ToString(...)
Expression conditionalCall = default(TProp) == null
? Expression.Condition(
Expression.Equal(propVar, Expression.Constant(null, typeof(TProp))),
Expression.Constant(null, typeof(string)),
toStringCall)
: toStringCall;
var block = Expression.Block(new[] { propVar },
Expression.Assign(propVar, Property.Body),
conditionalCall);
_cellTextFunc = Expression.Lambda<Func<TGridItem, string?>>(block, Property.Parameters[0]).Compile();
Just for fun, I put together a benchmark of the original approaches, Steve's proposed approach, and mine and MarcGravell's proposed solutions:
namespace MyBenchmarks
{
[MemoryDiagnoser]
public class Benchmark
{
private readonly GridItem gridItem = new();
private readonly Func<GridItem, string?> originalClass = new Original<GridItem, FormattableClass>() { Property = x => x.Class, Format = "X" }.Test();
private readonly Func<GridItem, string?> originalNonNullable = new Original<GridItem, FormattableStruct>() { Property = x => x.NonNullable, Format = "X" }.Test();
private readonly Func<GridItem, string?> originalNullable = new Original<GridItem, FormattableStruct?>() { Property = x => x.Nullable, Format = "X" }.Test();
private readonly Func<GridItem, string?> reflectionClass = new ReflectionTest<GridItem, FormattableClass>() { Property = x => x.Class, Format = "X" }.Test();
private readonly Func<GridItem, string?> reflectionNonNullable = new ReflectionTest<GridItem, FormattableStruct>() { Property = x => x.NonNullable, Format = "X" }.Test();
private readonly Func<GridItem, string?> reflectionNullable = new ReflectionTest<GridItem, FormattableStruct?>() { Property = x => x.Nullable, Format = "X" }.Test();
private readonly Func<GridItem, string?> formatterClass = new FormatterTest<GridItem, FormattableClass>() { Property = x => x.Class, Format = "X" }.Test();
private readonly Func<GridItem, string?> formatterNonNullable = new FormatterTest<GridItem, FormattableStruct>() { Property = x => x.NonNullable, Format = "X" }.Test();
private readonly Func<GridItem, string?> formatterNullable = new FormatterTest<GridItem, FormattableStruct?>() { Property = x => x.Nullable, Format = "X" }.Test();
private readonly Func<GridItem, string?> compiledExpressionClass = new CompiledExpressionTest<GridItem, FormattableClass>() { Property = x => x.Class, Format = "X" }.Test();
private readonly Func<GridItem, string?> compiledExpressionNonNullable = new CompiledExpressionTest<GridItem, FormattableStruct>() { Property = x => x.NonNullable, Format = "X" }.Test();
private readonly Func<GridItem, string?> compiledExpressionNullable = new CompiledExpressionTest<GridItem, FormattableStruct?>() { Property = x => x.Nullable, Format = "X" }.Test();
[Benchmark]
public void OriginalClass() => originalClass(gridItem);
[Benchmark]
public void OriginalNonNullable() => originalNonNullable(gridItem);
[Benchmark]
public void OriginalNullable() => originalNullable(gridItem);
[Benchmark]
public void ReflectionClass() => reflectionClass(gridItem);
[Benchmark]
public void ReflectionNonNullable() => reflectionNonNullable(gridItem);
[Benchmark]
public void ReflectionNullable() => reflectionNullable(gridItem);
[Benchmark]
public void FormatterClass() => formatterClass(gridItem);
[Benchmark]
public void FormatterNonNullable() => formatterNonNullable(gridItem);
[Benchmark]
public void FormatterNullable() => formatterNullable(gridItem);
[Benchmark]
public void CompiledExpressionClass() => compiledExpressionClass(gridItem);
[Benchmark]
public void CompiledExpressionNonNullable() => compiledExpressionNonNullable(gridItem);
[Benchmark]
public void CompiledExpressionNullable() => compiledExpressionNullable(gridItem);
}
class Original<TGridItem, TProp>
{
public Expression<Func<TGridItem, TProp>> Property { get; set; } = default!;
public string? Format { get; set; }
public Func<TGridItem, string?> Test()
{
Func<TGridItem, string?> cellTextFunc;
var compiledPropertyExpression = Property.Compile();
if (!string.IsNullOrEmpty(Format))
{
// 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
// If the type is nullable, we're interested in formatting the underlying type
var nullableUnderlyingTypeOrNull = Nullable.GetUnderlyingType(typeof(TProp));
if (!typeof(IFormattable).IsAssignableFrom(nullableUnderlyingTypeOrNull ?? typeof(TProp)))
{
throw new InvalidOperationException($"A '{nameof(Format)}' parameter was supplied, but the type '{typeof(TProp)}' does not implement '{typeof(IFormattable)}'.");
}
cellTextFunc = item => ((IFormattable?)compiledPropertyExpression!(item))?.ToString(Format, null);
}
else
{
cellTextFunc = item => compiledPropertyExpression!(item)?.ToString();
}
return cellTextFunc;
}
private string? FormatItem<U>(TGridItem item, Func<TGridItem, U> property) where U : IFormattable
{
return property(item)?.ToString(Format, null);
}
}
class ReflectionTest<TGridItem, TProp>
{
private static readonly MethodInfo formatItemMethodInfo = typeof(ReflectionTest<TGridItem, TProp>).GetMethod("FormatItem", BindingFlags.NonPublic | BindingFlags.Instance)!;
private static readonly MethodInfo formatItemNullableMethodInfo = typeof(ReflectionTest<TGridItem, TProp>).GetMethod("FormatItemNullable", BindingFlags.NonPublic | BindingFlags.Instance)!;
public Expression<Func<TGridItem, TProp>> Property { get; set; } = default!;
public string? Format { get; set; }
public Func<TGridItem, string?> Test()
{
Func<TGridItem, string?> cellTextFunc;
var compiledPropertyExpression = Property.Compile();
if (!string.IsNullOrEmpty(Format))
{
// If the type is nullable, we're interested in formatting the underlying type
var nullableUnderlyingTypeOrNull = Nullable.GetUnderlyingType(typeof(TProp));
if (!typeof(IFormattable).IsAssignableFrom(nullableUnderlyingTypeOrNull ?? typeof(TProp)))
{
throw new InvalidOperationException($"A '{nameof(Format)}' parameter was supplied, but the type '{typeof(TProp)}' does not implement '{typeof(IFormattable)}'.");
}
var formatter = (Func<TGridItem, Func<TGridItem, TProp>, string>)Delegate.CreateDelegate(
typeof(Func<TGridItem, Func<TGridItem, TProp>, string>),
this,
nullableUnderlyingTypeOrNull == null
? formatItemMethodInfo.MakeGenericMethod(typeof(TProp))
: formatItemNullableMethodInfo.MakeGenericMethod(nullableUnderlyingTypeOrNull));
cellTextFunc = item => formatter(item, compiledPropertyExpression);
}
else
{
cellTextFunc = item => compiledPropertyExpression!(item)?.ToString();
}
return cellTextFunc;
}
private string? FormatItem<U>(TGridItem item, Func<TGridItem, U> property) where U : IFormattable
{
return property(item)?.ToString(Format, null);
}
private string? FormatItemNullable<U>(TGridItem item, Func<TGridItem, U?> property) where U : struct, IFormattable
{
return property(item)?.ToString(Format, null);
}
}
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);
}
class FormatterTest<TGridItem, TProp>
{
public Expression<Func<TGridItem, TProp>> Property { get; set; } = default!;
public string? Format { get; set; }
public Func<TGridItem, string?> Test()
{
Func<TGridItem, string?> cellTextFunc;
var compiledPropertyExpression = Property.Compile();
if (!string.IsNullOrEmpty(Format))
{
// If the type is nullable, we're interested in formatting the underlying type
var nullableUnderlyingTypeOrNull = Nullable.GetUnderlyingType(typeof(TProp));
if (!typeof(IFormattable).IsAssignableFrom(nullableUnderlyingTypeOrNull ?? typeof(TProp)))
{
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);
}
else
{
cellTextFunc = item => compiledPropertyExpression!(item)?.ToString();
}
return cellTextFunc;
}
}
class CompiledExpressionTest<TGridItem, TProp>
{
private static readonly MethodInfo toStringMethod = typeof(IFormattable).GetMethod("ToString")!;
public Expression<Func<TGridItem, TProp>> Property { get; set; } = default!;
public string? Format { get; set; }
public Func<TGridItem, string?> Test()
{
Func<TGridItem, string?> cellTextFunc;
if (!string.IsNullOrEmpty(Format))
{
// If the type is nullable, we're interested in formatting the underlying type
var nullableUnderlyingTypeOrNull = Nullable.GetUnderlyingType(typeof(TProp));
if (!typeof(IFormattable).IsAssignableFrom(nullableUnderlyingTypeOrNull ?? typeof(TProp)))
{
throw new InvalidOperationException($"A '{nameof(Format)}' parameter was supplied, but the type '{typeof(TProp)}' does not implement '{typeof(IFormattable)}'.");
}
var propVar = Expression.Variable(typeof(TProp), "prop");
Expression toStringCallTarget = nullableUnderlyingTypeOrNull == null
? propVar
: Expression.Call(
propVar,
typeof(TProp).GetMethod("GetValueOrDefault", Type.EmptyTypes)!);
var toStringCall = Expression.Call(
toStringCallTarget,
toStringMethod,
Expression.Property(Expression.Constant(this), "Format"),
Expression.Constant(null, typeof(IFormatProvider)));
Expression conditionalCall = default(TProp) == null
? Expression.Condition(
Expression.Equal(propVar, Expression.Constant(null, typeof(TProp))),
Expression.Constant(null, typeof(string)),
toStringCall)
: toStringCall;
var block = Expression.Block(new[] { propVar },
Expression.Assign(propVar, Property.Body),
conditionalCall);
cellTextFunc = Expression.Lambda<Func<TGridItem, string?>>(block, Property.Parameters[0]).Compile();
}
else
{
cellTextFunc = item => Property.Compile()!(item)?.ToString();
}
return cellTextFunc;
}
}
public class GridItem
{
public FormattableClass Class { get; } = new FormattableClass();
public FormattableStruct NonNullable => new FormattableStruct();
public FormattableStruct? Nullable => new FormattableStruct();
}
public class FormattableClass : IFormattable
{
public string ToString(string? format, IFormatProvider? formatProvider) => "";
}
public struct FormattableStruct : IFormattable
{
public string ToString(string? format, IFormatProvider? formatProvider) => "";
}
public class Program
{
public static void Main(string[] args)
{
BenchmarkRunner.Run<Benchmark>();
}
}
}
With the results:
Method | Mean | Error | StdDev | Gen0 | Allocated |
---|---|---|---|---|---|
OriginalClass | 6.111 ns | 0.1206 ns | 0.1128 ns | - | - |
OriginalNonNullable | 7.568 ns | 0.1793 ns | 0.1677 ns | 0.0038 | 24 B |
OriginalNullable | 54.260 ns | 1.0880 ns | 1.4893 ns | 0.0038 | 24 B |
ReflectionClass | 6.750 ns | 0.0630 ns | 0.0590 ns | - | |
ReflectionNonNullable | 4.710 ns | 0.0514 ns | 0.0456 ns | - | |
ReflectionNullable | 7.374 ns | 0.0819 ns | 0.0726 ns | - | |
FormatterClass | 14.054 ns | 0.2079 ns | 0.1843 ns | - | - |
FormatterNonNullable | 3.521 ns | 0.0907 ns | 0.0849 ns | - | - |
FormatterNullable | 7.156 ns | 0.0889 ns | 0.0832 ns | - | - |
CompiledExpressionClass | 2.919 ns | 0.0864 ns | 0.0888 ns | - | - |
CompiledExpressionNonNullable | 1.815 ns | 0.0405 ns | 0.0379 ns | - | - |
CompiledExpressionNullable | 1.799 ns | 0.0577 ns | 0.0686 ns | - | - |
As you can see, the only solution which boxes is the original code. The reflection-based approach is faster than Marc's solution for classes, and about the same for structs. However, the expression-based approach is significantly faster than anything.
I've no idea where that extra cost in FormatterClass
is coming from, but it's repeatable.
I'm deliberately ignoring the cost of constructing the Func<TGridItem, string>
in this benchmark: the original code is obviously optimised for the case where Property
rarely changes, and the cost of compiling Property
is going to dominate anything else most likely.
Upvotes: 3
Reputation: 1063804
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);
}
Upvotes: 2