[英]How to use Reflection in order to avoid value boxing?
I am exploring Blazor's QuickGrid source code and found one interesting spot here .我正在探索 Blazor 的 QuickGrid源代码并在这里发现了一个有趣的地方。
On the 45th line Steve Sanderson left a TODO with a potentially better alternative solution.在第 45 行,Steve Sanderson 留下了一个 TODO 和一个可能更好的替代解决方案。
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 UPD-1:我所拥有的 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;
}
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). (我不得不添加
item
以使其有意义:也许我遗漏了一些东西)。
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>
.我们还需要为 nullable 定义一个单独的版本,因为
Nullable<T>
不能满足约束IFormattable<T>
,即使T
确实实现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:然后,一旦您知道
U
,就可以创建一个调用此方法的委托:
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
时使用它:
_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.这有一点前期成本(创建委托),但调用它很便宜,并且意味着如果它是值类型,您不必将
item
装箱到IFormattable
。
See on SharpLab . 请参阅 SharpLab 。
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.如果我要提出更好的解决方案,因为我们已经在使用带有
Property
的编译表达式,我会扩展它并将ToString
调用编译到表达式中。 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();
See it on SharpLab . 在 SharpLab 上查看。
Just for fun, I put together a benchmark of the original approaches, Steve's proposed approach, and mine and MarcGravell's proposed solutions:只是为了好玩,我将原始方法的基准、史蒂夫提出的方法以及我和 MarcGravell 提出的解决方案放在一起:
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.
基于反射的方法比 Marc 的类解决方案更快,对于结构来说也差不多。 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.我不知道
FormatterClass
中的额外成本从何而来,但它是可重复的。
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.我故意忽略了在此基准测试中构造
Func<TGridItem, string>
的成本:原始代码显然针对Property
很少更改的情况进行了优化,并且编译Property
的成本很可能会支配其他任何东西。
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;我会 go 有一个通用接口,例如没有通用约束的
IFormatter<T>
和一些带有必要投诉的私有实现,并在内部使用反射来决定使用哪个私有实现——一个简单的版本,除了Nullable<T>
之外的所有东西,以及一个特例; this is the same approach used by EqualityComparer<T>.Default
.这与
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).完整示例如下,注意
Formatter<T>.Instance
将是null
如果T
不支持IFormattable
(允许测试,尽管这也可以单独处理)。
The use of generics in the private implementation means we're using "constrained call", so: no boxing there.在私有实现中使用 generics 意味着我们正在使用“约束调用”,因此:那里没有装箱。 The special-casing of
Nullable<T>
means we can handle that too, with an additional indirection. Nullable<T>
的特殊外壳意味着我们也可以通过额外的间接处理来处理它。
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.重要的是我们只对每个类型进行一次思考——测试该类型是否可为空,它是否实现
IFormattable
等——然后通过策略实例进行缓存。 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):所提供示例的相关用法(请注意,您可能只需使用非通用 static class 和通用方法,这样您就可以执行
Formatter.Format(value, format, provider)
并使用来自value
的通用推理来完成其余部分):
// 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);
}
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.