-
-
Save dterracino/9f1444fb4aba1ffe5c6d2bd07bc9c8bf to your computer and use it in GitHub Desktop.
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
// ************** Implementation ************** | |
namespace CodeDucky | |
{ | |
using Microsoft.CSharp.RuntimeBinder; | |
using System; | |
using System.Collections; | |
using System.Collections.Concurrent; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Linq.Expressions; | |
using System.Reflection; | |
using System.Runtime.CompilerServices; | |
using System.Text; | |
using System.Text.RegularExpressions; | |
using System.Threading.Tasks; | |
using CSharpBinder = Microsoft.CSharp.RuntimeBinder.Binder; | |
public static class TypeHelpers | |
{ | |
#region ---- Explicit casts ---- | |
public static bool IsCastableTo(this Type from, Type to) | |
{ | |
// from http://www.codeducky.org/10-utilities-c-developers-should-know-part-one/ | |
Throw.IfNull(from, "from"); | |
Throw.IfNull(to, "to"); | |
// explicit conversion always works if to : from OR if | |
// there's an implicit conversion | |
if (from.IsAssignableFrom(to) || from.IsImplicitlyCastableTo(to)) | |
{ | |
return true; | |
} | |
var key = new KeyValuePair<Type, Type>(from, to); | |
bool cachedValue; | |
if (CastCache.TryGetCachedValue(key, out cachedValue)) | |
{ | |
return cachedValue; | |
} | |
// for nullable types, we can simply strip off the nullability and evaluate the underyling types | |
var underlyingFrom = Nullable.GetUnderlyingType(from); | |
var underlyingTo = Nullable.GetUnderlyingType(to); | |
if (underlyingFrom != null || underlyingTo != null) | |
{ | |
return (underlyingFrom ?? from).IsCastableTo(underlyingTo ?? to); | |
} | |
bool result; | |
if (from.IsValueType) | |
{ | |
try | |
{ | |
ReflectionHelpers.GetMethod(() => AttemptExplicitCast<object, object>()) | |
.GetGenericMethodDefinition() | |
.MakeGenericMethod(from, to) | |
.Invoke(null, new object[0]); | |
result = true; | |
} | |
catch (TargetInvocationException ex) | |
{ | |
result = !( | |
ex.InnerException is RuntimeBinderException | |
// if the code runs in an environment where this message is localized, we could attempt a known failure first and base the regex on it's message | |
&& Regex.IsMatch(ex.InnerException.Message, @"^Cannot convert type '.*' to '.*'$") | |
); | |
} | |
} | |
else | |
{ | |
// if the from type is null, the dynamic logic above won't be of any help because | |
// either both types are nullable and thus a runtime cast of null => null will | |
// succeed OR we get a runtime failure related to the inability to cast null to | |
// the desired type, which may or may not indicate an actual issue. thus, we do | |
// the work manually | |
result = from.IsNonValueTypeExplicitlyCastableTo(to); | |
} | |
CastCache.UpdateCache(key, result); | |
return result; | |
} | |
private static bool IsNonValueTypeExplicitlyCastableTo(this Type from, Type to) | |
{ | |
if ((to.IsInterface && !from.IsSealed) | |
|| (from.IsInterface && !to.IsSealed)) | |
{ | |
// any non-sealed type can be cast to any interface since the runtime type MIGHT implement | |
// that interface. The reverse is also true; we can cast to any non-sealed type from any interface | |
// since the runtime type that implements the interface might be a derived type of to. | |
return true; | |
} | |
// arrays are complex because of array covariance | |
// (see http://msmvps.com/blogs/jon_skeet/archive/2013/06/22/array-covariance-not-just-ugly-but-slow-too.aspx). | |
// Thus, we have to allow for things like var x = (IEnumerable<string>)new object[0]; | |
// and var x = (object[])default(IEnumerable<string>); | |
var arrayType = from.IsArray && !from.GetElementType().IsValueType ? from | |
: to.IsArray && !to.GetElementType().IsValueType ? to | |
: null; | |
if (arrayType != null) | |
{ | |
var genericInterfaceType = from.IsInterface && from.IsGenericType ? from | |
: to.IsInterface && to.IsGenericType ? to | |
: null; | |
if (genericInterfaceType != null) | |
{ | |
return arrayType.GetInterfaces() | |
.Any(i => i.IsGenericType | |
&& i.GetGenericTypeDefinition() == genericInterfaceType.GetGenericTypeDefinition() | |
&& i.GetGenericArguments().Zip(to.GetGenericArguments(), (ia, ta) => ta.IsAssignableFrom(ia) || ia.IsAssignableFrom(ta)).All(b => b)); | |
} | |
} | |
// look for conversion operators. Even though we already checked for implicit conversions, we have to look | |
// for operators of both types because, for example, if a class defines an implicit conversion to int then it can be explicitly | |
// cast to uint | |
const BindingFlags conversionFlags = BindingFlags.Public | BindingFlags.Static | BindingFlags.FlattenHierarchy; | |
var conversionMethods = from.GetMethods(conversionFlags) | |
.Concat(to.GetMethods(conversionFlags)) | |
.Where(m => (m.Name == "op_Explicit" || m.Name == "op_Implicit") | |
&& m.Attributes.HasFlag(MethodAttributes.SpecialName) | |
&& m.GetParameters().Length == 1 | |
&& ( | |
// the from argument of the conversion function can be an indirect match to from in | |
// either direction. For example, if we have A : B and Foo defines a conversion from B => Foo, | |
// then C# allows A to be cast to Foo | |
m.GetParameters()[0].ParameterType.IsAssignableFrom(from) | |
|| from.IsAssignableFrom(m.GetParameters()[0].ParameterType) | |
) | |
); | |
if (to.IsPrimitive && typeof(IConvertible).IsAssignableFrom(to)) | |
{ | |
// as mentioned above, primitive convertible types (i. e. not IntPtr) get special | |
// treatment in the sense that if you can convert from Foo => int, you can convert | |
// from Foo => double as well | |
return conversionMethods.Any(m => m.ReturnType.IsCastableTo(to)); | |
} | |
return conversionMethods.Any(m => m.ReturnType == to); | |
} | |
private static void AttemptExplicitCast<TFrom, TTo>() | |
{ | |
// based on the IL generated from | |
// var x = (TTo)(dynamic)default(TFrom); | |
var binder = CSharpBinder.Convert(CSharpBinderFlags.ConvertExplicit, typeof(TTo), typeof(TypeHelpers)); | |
var callSite = CallSite<Func<CallSite, TFrom, TTo>>.Create(binder); | |
callSite.Target(callSite, default(TFrom)); | |
} | |
#endregion | |
#region ---- Implicit casts ---- | |
public static bool IsImplicitlyCastableTo(this Type from, Type to) | |
{ | |
// from http://www.codeducky.org/10-utilities-c-developers-should-know-part-one/ | |
Throw.IfNull(from, "from"); | |
Throw.IfNull(to, "to"); | |
// not strictly necessary, but speeds things up and avoids polluting the cache | |
if (to.IsAssignableFrom(from)) | |
{ | |
return true; | |
} | |
var key = new KeyValuePair<Type, Type>(from, to); | |
bool cachedValue; | |
if (ImplicitCastCache.TryGetCachedValue(key, out cachedValue)) | |
{ | |
return cachedValue; | |
} | |
bool result; | |
try | |
{ | |
// overload of GetMethod() from http://www.codeducky.org/10-utilities-c-developers-should-know-part-two/ | |
// that takes Expression<Action> | |
ReflectionHelpers.GetMethod(() => AttemptImplicitCast<object, object>()) | |
.GetGenericMethodDefinition() | |
.MakeGenericMethod(from, to) | |
.Invoke(null, new object[0]); | |
result = true; | |
} | |
catch (TargetInvocationException ex) | |
{ | |
result = !( | |
ex.InnerException is RuntimeBinderException | |
// if the code runs in an environment where this message is localized, we could attempt a known failure first and base the regex on it's message | |
&& Regex.IsMatch(ex.InnerException.Message, @"^The best overloaded method match for 'System.Collections.Generic.List<.*>.Add(.*)' has some invalid arguments$") | |
); | |
} | |
ImplicitCastCache.UpdateCache(key, result); | |
return result; | |
} | |
private static void AttemptImplicitCast<TFrom, TTo>() | |
{ | |
// based on the IL produced by: | |
// dynamic list = new List<TTo>(); | |
// list.Add(default(TFrom)); | |
// We can't use the above code because it will mimic a cast in a generic method | |
// which doesn't have the same semantics as a cast in a non-generic method | |
var list = new List<TTo>(capacity: 1); | |
var binder = CSharpBinder.InvokeMember( | |
flags: CSharpBinderFlags.ResultDiscarded, | |
name: "Add", | |
typeArguments: null, | |
context: typeof(TypeHelpers), | |
argumentInfo: new[] | |
{ | |
CSharpArgumentInfo.Create(flags: CSharpArgumentInfoFlags.None, name: null), | |
CSharpArgumentInfo.Create( | |
flags: CSharpArgumentInfoFlags.UseCompileTimeType, | |
name: null | |
), | |
} | |
); | |
var callSite = CallSite<Action<CallSite, object, TFrom>>.Create(binder); | |
callSite.Target.Invoke(callSite, list, default(TFrom)); | |
} | |
#endregion | |
#region ---- Caching ---- | |
private const int MaxCacheSize = 5000; | |
private static readonly Dictionary<KeyValuePair<Type, Type>, bool> CastCache = new Dictionary<KeyValuePair<Type, Type>, bool>(), | |
ImplicitCastCache = new Dictionary<KeyValuePair<Type, Type>, bool>(); | |
private static bool TryGetCachedValue<TKey, TValue>(this Dictionary<TKey, TValue> cache, TKey key, out TValue value) | |
{ | |
lock (cache.As<ICollection>().SyncRoot) | |
{ | |
return cache.TryGetValue(key, out value); | |
} | |
} | |
private static void UpdateCache<TKey, TValue>(this Dictionary<TKey, TValue> cache, TKey key, TValue value) | |
{ | |
lock (cache.As<ICollection>().SyncRoot) | |
{ | |
if (cache.Count > MaxCacheSize) | |
{ | |
cache.Clear(); | |
} | |
cache[key] = value; | |
} | |
} | |
#endregion | |
} | |
} | |
// ************** Tests ************** | |
namespace CodeDucky | |
{ | |
using Microsoft.CSharp; | |
using Microsoft.VisualStudio.TestTools.UnitTesting; | |
using System; | |
using System.CodeDom.Compiler; | |
using System.Collections.Generic; | |
using System.Linq; | |
using System.Text; | |
using System.Threading.Tasks; | |
[TestClass] | |
public class ConversionsTest | |
{ | |
[TestMethod] | |
public void ImplicitlyCastable() | |
{ | |
this.RunTests((from, to) => from.IsImplicitlyCastableTo(to), @implicit: true); | |
} | |
[TestMethod] | |
public void ExplicitlyCastable() | |
{ | |
this.RunTests((from, to) => from.IsCastableTo(to), @implicit: false); | |
} | |
/// <summary> | |
/// Validates the given implementation function for either implicit or explicit conversion | |
/// </summary> | |
private void RunTests(Func<Type, Type, bool> func, bool @implicit) | |
{ | |
// gather types | |
var primitives = typeof(object).Assembly.GetTypes().Where(t => t.IsPrimitive).ToArray(); | |
var simpleTypes = new[] { typeof(string), typeof(DateTime), typeof(decimal), typeof(object), typeof(DateTimeOffset), typeof(TimeSpan), typeof(StringSplitOptions), typeof(DateTimeKind) }; | |
var variantTypes = new[] { typeof(string[]), typeof(object[]), typeof(IEnumerable<string>), typeof(IEnumerable<object>), typeof(Func<string>), typeof(Func<object>), typeof(Action<string>), typeof(Action<object>) }; | |
var conversionOperators = new[] { typeof(Operators), typeof(Operators2), typeof(DerivedOperators), typeof(OperatorsStruct) }; | |
var typesToConsider = primitives.Concat(simpleTypes).Concat(variantTypes).Concat(conversionOperators).ToArray(); | |
var allTypesToConsider = typesToConsider.Concat(typesToConsider.Where(t => t.IsValueType).Select(t => typeof(Nullable<>).MakeGenericType(t))); | |
// generate test cases | |
var cases = this.GenerateTestCases(allTypesToConsider, @implicit); | |
// collect errors | |
var mistakes = new List<string>(); | |
foreach (var @case in cases) | |
{ | |
var result = func(@case.Item1, @case.Item2); | |
if (result != (@case.Item3 == null)) | |
{ | |
// func(@case.Item1, @case.Item2); // break here for easy debugging | |
mistakes.Add(string.Format("{0} => {1}: got {2} for {3} cast", @case.Item1, @case.Item2, result, @implicit ? "implicit" : "explicit")); | |
} | |
} | |
Assert.IsTrue(mistakes.Count == 0, string.Join(Environment.NewLine, new[] { mistakes.Count + " errors" }.Concat(mistakes))); | |
} | |
private List<Tuple<Type, Type, CompilerError>> GenerateTestCases(IEnumerable<Type> types, bool @implicit) | |
{ | |
// gather all pairs | |
var typeCrossProduct = types.SelectMany(t => types, (from, to) => new { from, to }) | |
.Select((t, index) => new { t.from, t.to, index }) | |
.ToArray(); | |
// create the code to pass to the compiler | |
var code = string.Join( | |
Environment.NewLine, | |
new[] { "namespace A { public class B { static T Get<T>() { return default(T); } public void C() {" } | |
.Concat(typeCrossProduct.Select(t => string.Format("{0} var{1} = {2}Get<{3}>();", GetName(t.to), t.index, @implicit ? string.Empty : "(" + GetName(t.to) + ")", GetName(t.from)))) | |
.Concat(new[] { "}}}" }) | |
); | |
// compile the code | |
var provider = new CSharpCodeProvider(); | |
var compilerParams = new CompilerParameters(); | |
compilerParams.ReferencedAssemblies.Add(this.GetType().Assembly.Location); // reference the current assembly! | |
compilerParams.GenerateExecutable = false; | |
compilerParams.GenerateInMemory = true; | |
var compilationResult = provider.CompileAssemblyFromSource(compilerParams, code); | |
// determine the outcome of each conversion by matching compiler errors with conversions by line # | |
var cases = typeCrossProduct.GroupJoin( | |
compilationResult.Errors.Cast<CompilerError>(), | |
t => t.index, | |
e => e.Line - 2, | |
(t, e) => Tuple.Create(t.from, t.to, e.FirstOrDefault()) | |
) | |
.ToList(); | |
// add a special case | |
// this can't be verified by the normal means, since it's a private class | |
cases.Add(Tuple.Create(typeof(PrivateOperators), typeof(int), default(CompilerError))); | |
return cases; | |
} | |
/// <summary> | |
/// Gets a C# name for the given type | |
/// </summary> | |
private static string GetName(Type type) | |
{ | |
if (!type.IsGenericType) | |
{ | |
return type.ToString(); | |
} | |
return string.Format("{0}.{1}<{2}>", type.Namespace, type.Name.Substring(0, type.Name.IndexOf('`')), string.Join(", ", type.GetGenericArguments().Select(GetName))); | |
} | |
private class PrivateOperators | |
{ | |
public static implicit operator int(PrivateOperators o) | |
{ | |
return 1; | |
} | |
} | |
} | |
public class Operators | |
{ | |
public static implicit operator string(Operators o) | |
{ | |
throw new NotImplementedException(); | |
} | |
public static implicit operator int(Operators o) | |
{ | |
return 1; | |
} | |
public static explicit operator decimal?(Operators o) | |
{ | |
throw new NotImplementedException(); | |
} | |
public static explicit operator StringSplitOptions(Operators o) | |
{ | |
return StringSplitOptions.RemoveEmptyEntries; | |
} | |
} | |
public class DerivedOperators : Operators | |
{ | |
public static explicit operator DateTime(DerivedOperators o) | |
{ | |
return DateTime.Now; | |
} | |
} | |
public struct OperatorsStruct | |
{ | |
public static implicit operator string(OperatorsStruct o) | |
{ | |
throw new NotImplementedException(); | |
} | |
public static implicit operator int(OperatorsStruct o) | |
{ | |
return 1; | |
} | |
public static explicit operator decimal?(OperatorsStruct o) | |
{ | |
throw new NotImplementedException(); | |
} | |
public static explicit operator StringSplitOptions(OperatorsStruct o) | |
{ | |
return StringSplitOptions.RemoveEmptyEntries; | |
} | |
} | |
public class Operators2 | |
{ | |
public static explicit operator bool(Operators2 o) | |
{ | |
return false; | |
} | |
public static implicit operator Operators2(DerivedOperators o) | |
{ | |
return null; | |
} | |
public static explicit operator Operators2(int i) | |
{ | |
throw new NotImplementedException(); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment