Skip to content

Instantly share code, notes, and snippets.

@cameronism
Created May 11, 2017 16:48
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save cameronism/8702f71ef096ccc803eba77245730e4c to your computer and use it in GitHub Desktop.
Save cameronism/8702f71ef096ccc803eba77245730e4c to your computer and use it in GitHub Desktop.
ShallowValueComparer
void Main()
{
// http://stackoverflow.com/questions/263400/what-is-the-best-algorithm-for-an-overridden-system-object-gethashcode
Demo(new { }, new { });
Demo(null, new { });
Demo(new { }, null);
Demo(new { a = 1 }, new { a = 1 });
//new { _1 = HashInt(1), _2 = HashInt(2) }.Dump();
Demo(new { a = 1 }, new { a = 2 });
Demo(new { a = 1, b = "" }, new { a = 1, b = "" });
Demo(new { a = 1, b = "" }, new { a = 1, b = "b" });
Demo(new { a = 2, b = "" }, new { a = 1, b = "" });
Demo(new { a = 2, b = "" }, new { a = 1, b = "b" });
object x = null;
Demo(x, x);
Demo(x, new object());
x = new object();
Demo(x, x);
var y = new { a = 1, b = Guid.NewGuid(), c = DateTime.UtcNow };
Demo(y, y);
Demo(y, null);
Demo(null, y);
Demo(y, new { a = y.a, b = y.b, c = y.c });
Demo(new { a = 1, b = 1 }, new { a = 1, b = 1 });
Demo2(new { a = 1, b = 1 }, new { a = 1, b = 1, c = 3 });
Demo2(new { a = 1, b = 1 }, new { a = 1, b = 1, c = 3 });
var z = new A();
Demo(z, z);
//Demo(z, new A());
Demo(new { z }, new { z = new A() });
Demo(new { z, z1 = z }, new { z = new A(), z1 = z });
Demo(new { z, z1 = z, z2 = z }, new { z = new A(), z1 = z, z2 = z });
Demo(EqualityComparer<int>.Default, EqualityComparer<int>.Default);
Demo(new object(), new object(), expect: true);
}
//private int HashInt(int member)
//{
// unchecked
// {
// var hash = -2128831035;
// hash = ((hash * 16777619) ^ member.GetHashCode());
// return hash;
// }
//}
private void Demo<T>(T a, T b, bool? expect = null)
{
var actual = ShallowValueComparer.Equals(a, b);
var hash1 = ShallowValueComparer.GetHashCode(a);
var hash2 = ShallowValueComparer.GetHashCode(b);
var equal = expect ?? EqualityComparer<T>.Default.Equals(a, b);
var hashSuccess = equal == (hash1 == hash2);
new[] { a, b }.Dump("should be " + equal + $" reference: {object.ReferenceEquals(a, b)}");
if (actual != equal)
{
throw new Exception("failed");
}
if (!hashSuccess)
{
Util.Highlight("hash fail").Dump();
//throw new Exception("hash fail");
}
new
{
hash1,
hash2,
equal,
}.Dump();
}
private void Demo2(object a, object b)
{
var type = b?.GetType() ?? a?.GetType();
var actual = ShallowValueComparer.Equals(type, a, b);
var equal = Object.Equals(a, b);
new[] { a, b }.Dump("should be " + equal + $" reference: {object.ReferenceEquals(a, b)}");
if (actual != equal)
{
throw new Exception("failed");
}
}
class A
{
public override bool Equals(object o) => true;
public override int GetHashCode() => 0;
}
/*
Copyright 2017 Cameron Jordan
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
public abstract class ShallowValueComparer
{
private struct Methods
{
public Func<object, object, bool> EqualsMethod;
public Func<object, int> GetHashCodeMethod;
}
private class Generator
{
private Type _type;
private FieldInfo[] _fields;
private PropertyInfo[] _props;
private List<Expression> _equals = new List<Expression>();
private LabelTarget _equalsReturn = Expression.Label(typeof(bool));
private List<ParameterExpression> _equalsVariables = new List<ParameterExpression>();
private Dictionary<Type, (ParameterExpression, ParameterExpression)> _equalsMembers = new Dictionary<Type, (ParameterExpression, ParameterExpression)>();
private Dictionary<Type, ParameterExpression> _comparers = new Dictionary<Type, ParameterExpression>();
private List<Expression> _hash = new List<Expression>();
private Dictionary<Type, ParameterExpression> _hashMembers = new Dictionary<Type, ParameterExpression>();
private List<ParameterExpression> _hashVariables = new List<ParameterExpression>();
public Generator(Type type)
{
_type = type;
_fields = type.GetFields(BindingFlags.Public | BindingFlags.Instance);
_props = _type.GetProperties(BindingFlags.Public | BindingFlags.Instance);
// order of results from reflection IS NOT STABLE
// sort members by name so that hashcode is consistent between runs
// anything that changes sequence can (should) change hash code
// - member renames (that change relative order)
// - field to property (or vice-versa)
Array.Sort(_fields, (a, b) => StringComparer.OrdinalIgnoreCase.Compare(a.Name, b.Name));
Array.Sort(_props, (a, b) => StringComparer.OrdinalIgnoreCase.Compare(a.Name, b.Name));
}
public Methods Generate()
{
return new Methods
{
EqualsMethod = GenerateEquals(),
GetHashCodeMethod = GenerateGetHashCode(),
};
}
// not quite FNV from Jon Skeet http://stackoverflow.com/a/263416
private Func<object, int> GenerateGetHashCode()
{
var param = Expression.Parameter(typeof(object), "obj");
var value = Expression.Variable(_type, "value");
var hash = Expression.Variable(typeof(int), "hash");
_hashVariables.Add(value);
_hashVariables.Add(hash);
// let it throw if anybody called with a bad type
/* value = ({_type})obj */
/* var valueB = ({_type})b; */
_hash.Add(
Expression.Assign(
value,
Expression.Convert(
param,
_type)));
/* hash = 2166136261; */
_hash.Add(
Expression.Assign(
hash,
Expression.Constant(
unchecked((int)2166136261))));
foreach (var field in _fields)
{
HashMember(field.FieldType, field.Name, value, hash);
}
foreach (var prop in _props)
{
HashMember(prop.PropertyType, prop.Name, value, hash);
}
/* return hash; */
_hash.Add(hash);
var getHashCode = Expression.Lambda<Func<object, int>>(
Expression.Block(
typeof(int),
_hashVariables,
_hash),
param);
String.Join("\r\n", _hashVariables.Select(v => $"var {v.Name}: {v.Type}")).Dump();
String.Join("\r\n", _hash).Dump();
return getHashCode.Compile();
}
private void HashMember(Type type, string name, Expression value, Expression hash)
{
var member = GetHashVariables(type);
/* member = value.{name}; */
_hash.Add(
Expression.Assign(
member,
Expression.PropertyOrField(
value,
name)));
var method = type.GetMethod("GetHashCode", BindingFlags.Instance | BindingFlags.Public, null, CallingConventions.Standard, Type.EmptyTypes, null);
/* member.GetHashCode() */
//Expression call = Dump("actual hash code", Expression.Call(member, method));
Expression call = Expression.Call(member, method);
// See if a null check required before GetHashCode()
if (Nullable.GetUnderlyingType(type) != null || type.IsInterface || type.IsClass)
{
/* member == null ? 0 : member.GetHashCode() */
call =
Expression.Condition(
Expression.Equal(member, Expression.Constant(null, type)),
Expression.Constant((int)0),
call);
}
/* hash = (hash * 16777619) ^ member.GetHashCode(); */
_hash.Add(
Expression.Assign(
hash,
Expression.ExclusiveOr(
Expression.Multiply(hash, Expression.Constant((int)16777619)),
call)));
}
private Func<object, object, bool> GenerateEquals()
{
var paramA = Expression.Parameter(typeof(object), "a");
var paramB = Expression.Parameter(typeof(object), "B");
/* if (object.ReferenceEquals(a, b)) return true; */
_equals.Add(
Expression.IfThen(
Expression.ReferenceEqual(
paramA,
paramB),
Expression.Return(
_equalsReturn,
Expression.Constant(true))));
// object.ReferenceEquals would have caught it if both are null
/* if (a == null || b == null || !(a is {_type}) || !(b is {_type})) return false; */
_equals.Add(
Expression.IfThen(
BinaryExpression(
Expression.OrElse,
Expression.Equal(paramA, Expression.Constant(null, typeof(object))),
Expression.Equal(paramB, Expression.Constant(null, typeof(object))),
Expression.Not(Expression.TypeIs(paramA, _type)),
Expression.Not(Expression.TypeIs(paramB, _type))),
Expression.Return(
_equalsReturn,
Expression.Constant(false))));
var valueA = Expression.Variable(_type, "valueA");
var valueB = Expression.Variable(_type, "valueB");
_equalsVariables.Add(valueA);
_equalsVariables.Add(valueB);
/* var valueA = ({_type})a; */
_equals.Add(
Expression.Assign(
valueA,
Expression.Convert(
paramA,
_type)));
/* var valueB = ({_type})b; */
_equals.Add(
Expression.Assign(
valueB,
Expression.Convert(
paramB,
_type)));
foreach (var field in _fields)
{
CompareMember(field.FieldType, field.Name, valueA, valueB);
}
foreach (var prop in _props)
{
CompareMember(prop.PropertyType, prop.Name, valueA, valueB);
}
// return true
_equals.Add(Expression.Label(_equalsReturn, Expression.Constant(true)));
var equals = Expression.Lambda<Func<object, object, bool>>(
Expression.Block(
typeof(bool),
_equalsVariables,
_equals),
paramA,
paramB);
//
// String.Join("\r\n", _equalsVariables.Select(v => $"var {v.Name}: {v.Type}")).Dump();
// String.Join("\r\n", _equals).Dump();
return equals.Compile();
}
private void CompareMember(Type type, string name, Expression valueA, Expression valueB)
{
var (memberA, memberB) = GetEqualsVariables(type);
/* memberA = valueA.{field}; */
_equals.Add(
Expression.Assign(
memberA,
Expression.PropertyOrField(
valueA,
name)));
/* memberB = valueB.{field}; */
_equals.Add(
Expression.Assign(
memberB,
Expression.PropertyOrField(
valueB,
name)));
/* if (memberA != memberB) return false */
_equals.Add(
Expression.IfThen(
NotEqual(type, memberA, memberB),
Expression.Return(
_equalsReturn,
Expression.Constant(false))));
}
private Expression NotEqual(Type type, Expression memberA, Expression memberB)
{
var primitive = Type.GetTypeCode(Nullable.GetUnderlyingType(type) ?? type) != TypeCode.Object;
// start here until it makes me do better
if (primitive)
{
/* memberA != memberB */
return Expression.NotEqual(
memberA,
memberB);
}
var comparerType = typeof(EqualityComparer<>).MakeGenericType(type);
if (!_comparers.TryGetValue(type, out var comparer))
{
comparer = Expression.Variable(comparerType, "comparer");
_equalsVariables.Add(comparer);
_comparers[type] = comparer;
/* comparer = EqualityComparer<{type}>.Default */
_equals.Add(
Expression.Assign(
comparer,
Expression.Property(
null,
comparerType.GetProperty("Default"))));
}
/* !comparer.Equals(memberA, memberB) */
return Expression.Not(
Expression.Call(
comparer,
comparerType.GetMethod("Equals", new[] { type, type }),
memberA,
memberB));
}
private (ParameterExpression, ParameterExpression) GetEqualsVariables(Type type)
{
if (_equalsMembers.TryGetValue(type, out var vars))
{
return vars;
}
vars = (Expression.Variable(type, "memberA"), Expression.Variable(type, "memberB"));
_equalsVariables.Add(vars.Item1);
_equalsVariables.Add(vars.Item2);
_equalsMembers[type] = vars;
return vars;
}
private ParameterExpression GetHashVariables(Type type)
{
if (_hashMembers.TryGetValue(type, out var member))
{
return member;
}
member = Expression.Variable(type, "member");
_hashVariables.Add(member);
_hashMembers[type] = member;
return member;
}
// private Expression Dump(string message, Expression e)
// {
// return Expression.Call(_dump.MakeGenericMethod(e.Type), e, Expression.Constant(message, typeof(string)));
// }
static BinaryExpression BinaryExpression(Func<Expression, Expression, BinaryExpression> binary, params Expression[] expressions)
{
var e = binary(expressions[0], expressions[1]);
for (var i = 2; i < expressions.Length; i++)
{
e = binary(e, expressions[i]);
}
return e;
}
}
private static Dictionary<Type, Methods> _methods = new Dictionary<Type, Methods>();
public override bool Equals(object obj) => Equals(GetType(), this, obj);
public static bool Equals<T>(T a, T b) => Equals(typeof(T), a, b);
public static bool Equals(Type type, object a, object b) => GetMethods(type).EqualsMethod(a, b);
public override int GetHashCode() => GetHashCode(GetType(), this);
public static int GetHashCode<T>(T obj) => GetHashCode(typeof(T), obj);
public static int GetHashCode(Type type, object obj) => obj == null ? 0 : GetMethods(type).GetHashCodeMethod(obj);
private static Methods GetMethods(Type type)
{
Methods m;
bool found;
lock (_methods)
{
found = _methods.TryGetValue(type, out m);
}
if (!found)
{
m = GenerateAndSave(type);
}
return m;
}
private static Methods GenerateAndSave(Type type)
{
var m = new Generator(type).Generate();
lock (_methods)
{
_methods[type] = m;
}
return m;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment