AlexanderIvanov.LazyEqualities
1.2.0
dotnet add package AlexanderIvanov.LazyEqualities --version 1.2.0
NuGet\Install-Package AlexanderIvanov.LazyEqualities -Version 1.2.0
<PackageReference Include="AlexanderIvanov.LazyEqualities" Version="1.2.0" />
paket add AlexanderIvanov.LazyEqualities --version 1.2.0
#r "nuget: AlexanderIvanov.LazyEqualities, 1.2.0"
// Install AlexanderIvanov.LazyEqualities as a Cake Addin #addin nuget:?package=AlexanderIvanov.LazyEqualities&version=1.2.0 // Install AlexanderIvanov.LazyEqualities as a Cake Tool #tool nuget:?package=AlexanderIvanov.LazyEqualities&version=1.2.0
Lazy Equalities
Lazy Equalities is a simple to use utility library designed to ease .NET developers in implementing object comparison functionality: Equals(object obj)
, ==
and !=
. Since doing so error-freely for many types is a tedious task of writing lines upon lines of generic boilerplate code, Lazy Equalities aims to do (part of) the job for you.
Some Examples
An example of Lazy Equality comparison with minimal boilerplate:
using AlexanderIvanov.LazyEqualities;
class SomeClass
{
public int X { get; }
public int Y { get; }
public AnotherClass Z { get; }
public int NewlyAddedProperty { get; }
public override int GetHashCode() => base.GetHashCode();
public override bool Equals(object obj) => this == obj as SomeClass;
public static bool operator ==(SomeClass x, SomeClass y) => LazyEquality.Equals(x, y);
public static bool operator !=(SomeClass x, SomeClass y) => LazyEquality.NotEquals(x, y);
}
//Neater, isn't it?
Usage in details
Comparison Rules
- All definitions required for the usage of this library are located in the
AlexanderIvanov.LazyEqualities
namespace. - The
Equals
andNotEquals
methods having a generic interface like this:
public static bool LazyEquality.Equals<T> (T x, T y)
and will be referred to as 'the two comparison methods'. - Comparison of
x
andy
will be performed according to the first matched rule:- The value of
x==y
orx!=y
will be returned if the generic parameter is a primitive type, string or an enum type. - If T implements the
System.IEquatable<T>
interface:- If the defining class of
T
has anEquatableDependsOnLazyComparisonAttrobute
attribute, theIEquatable<T>
interface of T will NOT be considered. - Otherwise, the system will consider the absence of such attribute:
- If this is NOT a direct invocation of one of the two comparison methods, the value of
(x as IEquatable<T>).Equals(y)
or its opposite will be returned. - If this is a direct invocation of one of the two comparison methods, an exception will be thrown.
- This attribute implies that the implementation of
IEquatable<T>
depends on Lazy Equalities. Its lack thereof means that the implementation is either a native boilerplate or uses another library. The above exception is in place in order to prevent a stack overflow whenLazyEquality.Equals<T>(T, T)
is called and in turn it calls itself.
- If this is NOT a direct invocation of one of the two comparison methods, the value of
- If the defining class of
- If
T
implements aSystem.Collections.Generic.IEnumerable<TItem>
interface or is such interface itself:- A sequential equality check will be performed, each item being compared (Recursively, following the same ruleset)
- Two collections with a different item count will never be considered equal.
- If T implements
IEnumerable<TItem>
for more than one differentTItem
, only the first one will be considered.
- If T only implements the non-generic
System.Collections.IEnumerable
interface:- A sequential equality check will be performed, each item being compared with the default
x.CompareTo(object obj)
method (not recursively), whether or not it's been overridden and implemented somehow (using lazy equalities, boilerplate code or otherwise); - Two collections with a different item count will never be considered equal.
- A sequential equality check will be performed, each item being compared with the default
- In all other cases, a recursive memberwise comparison will be performed:
- The values of all non-static properties with a getter that do not have an
EqualityExcludeAttribute
will be considered. - The values of all non-static fields that do have an
EqualityIncludeAttribute
will be considered
- The values of all non-static properties with a getter that do not have an
- The value of
Some Examples (Constructors omitted)
using AlexanderIvanov.LazyEqualities;
using System.Threading;
using System;
using ...;
public class SomeClass
{
//This field will not be considered because it's static
private static IDCounter = 0;
//...And neither will this static property
static string Identifier{ get=>nameof(SomeClass); set=>throw new Exception("WtfuDo?"); }
//this field will NOT be considered (implicitly, because it's a field)
internal int objectID= Interlocked.Increment(ref IDCounter);
//but this one will (explicitly, using an attribute)
[EqualityInclude]
private int x=12;
//These three properties will be considered (implicitly)
protected internal int X { get; internal set; }
private int Y { get; set; }
public AnotherClass Z { get; } //The value of this one will be compared recursively
//This property will not be considered (explicitly, using an attribute)
[EqualityExclude]
public int NewlyAddedProperty { get; protected set; }
//This property will not be considered because it doesn't have a getter.
internal ISomeInterface SetterInjection{ set=> Console.WriteLine("Please don't do this."); }
//It's good practice for non-internal classes to implement these for more seamless
//comparisons.
public override int GetHashCode() => base.GetHashCode();
public override bool Equals(object obj) => this == obj as SomeClass;
public static bool operator ==(SomeClass x, SomeClass y) => LazyEquality.Equals(x, y);
public static bool operator !=(SomeClass x, SomeClass y) => LazyEquality.NotEquals(x, y);
}
//This class doesn't have an EquatableDependsOnLazyComparisonAttribute
//Because the Equals method doesn't use the lazy comparison library.
internal class NativeEquatable : IEquatable<NativeEquatable>
{
private string str;
public bool Equals(NativeEquatable other) => str.Equals(other.str);
}
//But this one does because it...does!
[EquatableDependsOnLazyComparisonAttribute]
internal class NativeEquatableWrapper : IEquatable<NativeEquatableWrapper>
{
//this one will be compared using ==
public string StringValue { get; }
//this one will be compared using IEquatable<NativeEquatable>.Equals(NativeEquatable).
public NativeEquatable Native { get; }
public bool Equals(NativeEquatableWrapper other) =>
LazyEquality.Equals(this, other);//<---RIGHT HERE
}
How it works
On every invocation of a comparison method, a corresponding NotEquals<T>.Compare(T x, T y)
method will be called. Note that the internal static NotEquals<T>
is generic which allows per-type delegate storage using a static field initialized upon first usage.
Said delegates are compiled using reflection and expression trees (once per type) . Reflection is not used upon further comparisons for the same type. This delegate caching technique is often used by dependency injection (DI) frameworks. It is to be expected that performance it always hurts the first time but subsequent invocations are seamless.
The LazyEquality
class also exposes the EnsureInitialized(Type type)
method which will ensure the comparator delegate for type
has been initialized.
This library is written under .Net Standard 2.0 and should work across .Net Core and recent versions of the Windows-only .Net.
Links
© Alexander "startrunner" Ivanov 2017 - 2018
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. |
.NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
.NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
.NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
MonoAndroid | monoandroid was computed. |
MonoMac | monomac was computed. |
MonoTouch | monotouch was computed. |
Tizen | tizen40 was computed. tizen60 was computed. |
Xamarin.iOS | xamarinios was computed. |
Xamarin.Mac | xamarinmac was computed. |
Xamarin.TVOS | xamarintvos was computed. |
Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
Early Release