Hello!
The idea is to use lazy cached properties everywhere in immutable objects, where we would normally use processor-heavy methods with no arguments. And the article - how to design it and and why.
Accessing a lazy property of an object visually
:
1) - , — ,
2) (, SRP)
3) ,
TL; DR at the very bottom.
Why evil?
public sealed record Integer(int Value);
Value
int
. , :
public sealed record Integer(int Value)
{
public Integer Triple() => new Integer(Value * 3);
}
, , . ,
public int SomeMethod(Integer number)
{
var tripled = number.Triple();
if (tripled.Value > 5)
return tripled.Value;
else
return 1;
}
,
public int SomeMethod(Integer number)
=> number.Tripled > 5 ? number.Tripled.Value : 1;
, , , . , , Tripled
.
?
- . , , .
- . , , ( — ).
- . immutable object, ,
Equals
GetHashCode
, - , .
, , . , :
public sealed record Number(int Value)
{
public int Number Tripled => tripled.GetValue(@this => new Number(@this.Value * 3), @this);
private FieldCache<Number> tripled;
}
, , Cacheable
. source- , - . — , .
:
1 ( ?):
public sealed record Number(int Value)
{
public int Number Tripled => new Number(@this.Value * 3);
}
( )
2 ( Lazy<T>
):
public sealed record Number : IEquatable<Number>
{
public int Value { get; init; } // ,
public int Number Tripled => tripled.Value;
private Lazy<Number> tripled;
public Number(int value)
{
Value = value;
tripled = new(() => value * 3); // , this-
}
// Equals, , , Lazy<T>
public bool Equals(Number number) => Value == number.Value;
// GetHashCode
public override int GetHashCode() => Value.GetHashCode();
}
, . , ? , .
, with
, , (-). Lazy
, .
3 ( ConditionalWeakTable
):
public sealed record Number
{
public Number Tripled => tripled.GetValue(this, @this => new Integer(@this.Value * 3));
private static ConditionalWeakTable<Number, Number> tripled = new();
}
. ValueType ConditionalWeakTable
-. , - ( , , 6 , , ).
4 ( ):
public sealed record Number
{
public int Value { get; init; }
public Number Tripled { get; }
public Number(int value)
{
Value = value;
Tripled = new Number { Value = value * 3 };
}
}
stackoverflow
, , "" — , .
- , , .
Equals
GetHashCode
true
0
. , , . ,Equals
GetHashCode
, .- . , , .
- ,
GetValue
, ,ConditionalWeakTable
. -,Lazy<T>
. -
with
,initialized
holder
, — .
!
, :
public struct FieldCache<T> : IEquatable<FieldCache<T>>
{
private T value;
private object holder; // , generic
// , , Equals
public bool Equals(FieldCache<T> _) => true;
public override int GetHashCode() => 0;
}
GetValue
:
public struct FieldCache<T> : IEquatable<FieldCache<T>>
{
public T GetValue<TThis>(Func<TThis, T> factory, TThis @this) where TThis : class // record - . ,
{
// (, - null)
if (!ReferenceEquals(@this, holder))
lock (@this)
{
if (!ReferenceEquals(@this, holder))
{
// , FieldCache , - . , , ,
value = factory(@this);
holder = @this;
}
}
return value;
}
}
, :
public sealed record Number(int Value)
{
public int Number Tripled => tripled.GetValue(@this => new Number(@this.Value * 3), @this);
private FieldCache<Number> tripled;
}
, , FieldCache
— Lazy<T>
.
Method | Mean |
---|---|
BenchFunction | 4,599.1638 ns |
Lazy | 0.6717 ns |
FieldCache | 3.6674 ns |
ConditionalWeakTable | 25.0521 ns |
BenchFunction
— - , , . . , FieldCache<T>
, Lazy<T>
.
, , , .
TL;DR
: , .
And the well-known existing approaches, apparently, do not allow it to be done beautifully, so you have to write your own.