Created
May 11, 2017 16:48
-
-
Save cameronism/8702f71ef096ccc803eba77245730e4c to your computer and use it in GitHub Desktop.
ShallowValueComparer
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
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