using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Reflection; using UnityEngine; namespace Unity.VisualScripting { public static class ConversionUtility { public enum ConversionType { Impossible, Identity, Upcast, Downcast, NumericImplicit, NumericExplicit, UserDefinedImplicit, UserDefinedExplicit, UserDefinedThenNumericImplicit, UserDefinedThenNumericExplicit, UnityHierarchy, EnumerableToArray, EnumerableToList, ToString } private const BindingFlags UserDefinedBindingFlags = BindingFlags.Static | BindingFlags.Public; private static readonly Dictionary conversionTypesCache = new Dictionary(new ConversionQueryComparer()); private static readonly Dictionary userConversionMethodsCache = new Dictionary(new ConversionQueryComparer()); private static bool RespectsIdentity(Type source, Type destination) { return source == destination; } private static bool IsUpcast(Type source, Type destination) { return destination.IsAssignableFrom(source); } private static bool IsDowncast(Type source, Type destination) { return source.IsAssignableFrom(destination); } private static bool ExpectsString(Type source, Type destination) { return destination == typeof(string); } public static bool HasImplicitNumericConversion(Type source, Type destination) { return implicitNumericConversions.ContainsKey(source) && implicitNumericConversions[source].Contains(destination); } public static bool HasExplicitNumericConversion(Type source, Type destination) { return explicitNumericConversions.ContainsKey(source) && explicitNumericConversions[source].Contains(destination); } public static bool HasNumericConversion(Type source, Type destination) { return HasImplicitNumericConversion(source, destination) || HasExplicitNumericConversion(source, destination); } private static IEnumerable FindUserDefinedConversionMethods(ConversionQuery query) { var source = query.source; var destination = query.destination; var sourceMethods = source.GetMethods(UserDefinedBindingFlags) .Where(m => m.IsUserDefinedConversion()); var destinationMethods = destination.GetMethods(UserDefinedBindingFlags) .Where(m => m.IsUserDefinedConversion()); return sourceMethods.Concat(destinationMethods).Where ( m => m.GetParameters()[0].ParameterType.IsAssignableFrom(source) || source.IsAssignableFrom(m.GetParameters()[0].ParameterType) ); } // Returning an array directly so that the enumeration in // UserDefinedConversion does not allocate memory private static MethodInfo[] GetUserDefinedConversionMethods(Type source, Type destination) { var query = new ConversionQuery(source, destination); if (!userConversionMethodsCache.ContainsKey(query)) { userConversionMethodsCache.Add(query, FindUserDefinedConversionMethods(query).ToArray()); } return userConversionMethodsCache[query]; } private static ConversionType GetUserDefinedConversionType(Type source, Type destination) { var conversionMethods = GetUserDefinedConversionMethods(source, destination); // Duplicate user defined conversions are not allowed, so FirstOrDefault is safe. // Look for direct conversions. var conversionMethod = conversionMethods.FirstOrDefault(m => m.ReturnType == destination); if (conversionMethod != null) { if (conversionMethod.Name == "op_Implicit") { return ConversionType.UserDefinedImplicit; } else if (conversionMethod.Name == "op_Explicit") { return ConversionType.UserDefinedExplicit; } } // Primitive types can skip the middleman cast, even if it is explicit. else if (destination.IsPrimitive && destination != typeof(IntPtr) && destination != typeof(UIntPtr)) { // Look for implicit conversions. conversionMethod = conversionMethods.FirstOrDefault(m => HasImplicitNumericConversion(m.ReturnType, destination)); if (conversionMethod != null) { if (conversionMethod.Name == "op_Implicit") { return ConversionType.UserDefinedThenNumericImplicit; } else if (conversionMethod.Name == "op_Explicit") { return ConversionType.UserDefinedThenNumericExplicit; } } // Look for explicit conversions. else { conversionMethod = conversionMethods.FirstOrDefault(m => HasExplicitNumericConversion(m.ReturnType, destination)); if (conversionMethod != null) { return ConversionType.UserDefinedThenNumericExplicit; } } } return ConversionType.Impossible; } private static bool HasEnumerableToArrayConversion(Type source, Type destination) { return source != typeof(string) && typeof(IEnumerable).IsAssignableFrom(source) && destination.IsArray && destination.GetArrayRank() == 1; } private static bool HasEnumerableToListConversion(Type source, Type destination) { return source != typeof(string) && typeof(IEnumerable).IsAssignableFrom(source) && destination.IsGenericType && destination.GetGenericTypeDefinition() == typeof(List<>); } private static bool HasUnityHierarchyConversion(Type source, Type destination) { if (destination == typeof(GameObject)) { return typeof(Component).IsAssignableFrom(source); } else if (typeof(Component).IsAssignableFrom(destination) || destination.IsInterface) { return source == typeof(GameObject) || typeof(Component).IsAssignableFrom(source); } return false; } private static bool IsValidConversion(ConversionType conversionType, bool guaranteed) { if (conversionType == ConversionType.Impossible) { return false; } if (guaranteed) { // Downcasts are not guaranteed to succeed. if (conversionType == ConversionType.Downcast) { return false; } } return true; } public static bool CanConvert(object value, Type type, bool guaranteed) { return IsValidConversion(GetRequiredConversion(value, type), guaranteed); } public static bool CanConvert(Type source, Type destination, bool guaranteed) { return IsValidConversion(GetRequiredConversion(source, destination), guaranteed); } public static object Convert(object value, Type type) { return Convert(value, type, GetRequiredConversion(value, type)); } public static T Convert(object value) { return (T)Convert(value, typeof(T)); } public static bool TryConvert(object value, Type type, out object result, bool guaranteed) { var conversionType = GetRequiredConversion(value, type); if (IsValidConversion(conversionType, guaranteed)) { result = Convert(value, type, conversionType); return true; } result = value; return false; } public static bool TryConvert(object value, out T result, bool guaranteed) { if (TryConvert(value, typeof(T), out var res, guaranteed)) { result = (T)res; return true; } result = default; return false; } public static bool IsConvertibleTo(this Type source, Type destination, bool guaranteed) { return CanConvert(source, destination, guaranteed); } public static bool IsConvertibleTo(this object source, Type type, bool guaranteed) { return CanConvert(source, type, guaranteed); } public static bool IsConvertibleTo(this object source, bool guaranteed) { return CanConvert(source, typeof(T), guaranteed); } public static object ConvertTo(this object source, Type type) { return Convert(source, type); } public static T ConvertTo(this object source) { return (T)Convert(source, typeof(T)); } public static ConversionType GetRequiredConversion(Type source, Type destination) { var query = new ConversionQuery(source, destination); if (!conversionTypesCache.TryGetValue(query, out var conversionType)) { conversionType = DetermineConversionType(query); conversionTypesCache.Add(query, conversionType); } return conversionType; } private static ConversionType DetermineConversionType(ConversionQuery query) { var source = query.source; var destination = query.destination; if (source == null) { if (destination.IsNullable()) { return ConversionType.Identity; } else { return ConversionType.Impossible; } } Ensure.That(nameof(destination)).IsNotNull(destination); if (RespectsIdentity(source, destination)) { return ConversionType.Identity; } else if (IsUpcast(source, destination)) { return ConversionType.Upcast; } else if (IsDowncast(source, destination)) { return ConversionType.Downcast; } // Disabling *.ToString conversion, because it's more often than otherwise very confusing /*else if (ExpectsString(source, destination)) { return ConversionType.ToString; }*/ else if (HasImplicitNumericConversion(source, destination)) { return ConversionType.NumericImplicit; } else if (HasExplicitNumericConversion(source, destination)) { return ConversionType.NumericExplicit; } else if (HasUnityHierarchyConversion(source, destination)) { return ConversionType.UnityHierarchy; } else if (HasEnumerableToArrayConversion(source, destination)) { return ConversionType.EnumerableToArray; } else if (HasEnumerableToListConversion(source, destination)) { return ConversionType.EnumerableToList; } else { var userDefinedConversionType = GetUserDefinedConversionType(source, destination); if (userDefinedConversionType != ConversionType.Impossible) { return userDefinedConversionType; } } return ConversionType.Impossible; } public static ConversionType GetRequiredConversion(object value, Type type) { Ensure.That(nameof(type)).IsNotNull(type); return GetRequiredConversion(value?.GetType(), type); } private static object NumericConversion(object value, Type type) { return System.Convert.ChangeType(value, type); } private static object UserDefinedConversion(ConversionType conversion, object value, Type type) { var valueType = value.GetType(); var conversionMethods = GetUserDefinedConversionMethods(valueType, type); var numeric = conversion == ConversionType.UserDefinedThenNumericImplicit || conversion == ConversionType.UserDefinedThenNumericExplicit; MethodInfo conversionMethod = null; if (numeric) { foreach (var m in conversionMethods) { if (HasNumericConversion(m.ReturnType, type)) { conversionMethod = m; break; } } } else { foreach (var m in conversionMethods) { if (m.ReturnType == type) { conversionMethod = m; break; } } } var result = conversionMethod.InvokeOptimized(null, value); if (numeric) { result = NumericConversion(result, type); } return result; } private static object EnumerableToArrayConversion(object value, Type arrayType) { var elementType = arrayType.GetElementType(); var objectArray = ((IEnumerable)value).Cast().Where(elementType.IsAssignableFrom).ToArray(); // Non-generic OfType var typedArray = Array.CreateInstance(elementType, objectArray.Length); objectArray.CopyTo(typedArray, 0); return typedArray; } private static object EnumerableToListConversion(object value, Type listType) { var elementType = listType.GetGenericArguments()[0]; var objectArray = ((IEnumerable)value).Cast().Where(elementType.IsAssignableFrom).ToArray(); // Non-generic OfType var typedList = (IList)Activator.CreateInstance(listType); for (var i = 0; i < objectArray.Length; i++) { typedList.Add(objectArray[i]); } return typedList; } private static object UnityHierarchyConversion(object value, Type type) { if (value.IsUnityNull()) { return null; } if (type == typeof(GameObject) && value is Component) { return ((Component)value).gameObject; } else if (typeof(Component).IsAssignableFrom(type) || type.IsInterface) { if (value is Component) { return ((Component)value).GetComponent(type); } else if (value is GameObject) { return ((GameObject)value).GetComponent(type); } } throw new InvalidConversionException(); } private static object Convert(object value, Type type, ConversionType conversionType) { Ensure.That(nameof(type)).IsNotNull(type); if (conversionType == ConversionType.Impossible) { throw new InvalidConversionException($"Cannot convert from '{value?.GetType().ToString() ?? "null"}' to '{type}'."); } try { switch (conversionType) { case ConversionType.Identity: case ConversionType.Upcast: case ConversionType.Downcast: return value; case ConversionType.ToString: return value.ToString(); case ConversionType.NumericImplicit: case ConversionType.NumericExplicit: return NumericConversion(value, type); case ConversionType.UserDefinedImplicit: case ConversionType.UserDefinedExplicit: case ConversionType.UserDefinedThenNumericImplicit: case ConversionType.UserDefinedThenNumericExplicit: return UserDefinedConversion(conversionType, value, type); case ConversionType.EnumerableToArray: return EnumerableToArrayConversion(value, type); case ConversionType.EnumerableToList: return EnumerableToListConversion(value, type); case ConversionType.UnityHierarchy: return UnityHierarchyConversion(value, type); default: throw new UnexpectedEnumValueException(conversionType); } } catch (Exception ex) { throw new InvalidConversionException($"Failed to convert from '{value?.GetType().ToString() ?? "null"}' to '{type}' via {conversionType}.", ex); } } private struct ConversionQuery : IEquatable { public readonly Type source; public readonly Type destination; public ConversionQuery(Type source, Type destination) { this.source = source; this.destination = destination; } public bool Equals(ConversionQuery other) { return source == other.source && destination == other.destination; } public override bool Equals(object obj) { if (!(obj is ConversionQuery)) { return false; } return Equals((ConversionQuery)obj); } public override int GetHashCode() { return HashUtility.GetHashCode(source, destination); } } // Make sure the equality comparer doesn't use boxing private struct ConversionQueryComparer : IEqualityComparer { public bool Equals(ConversionQuery x, ConversionQuery y) { return x.Equals(y); } public int GetHashCode(ConversionQuery obj) { return obj.GetHashCode(); } } #region Numeric Conversions // https://msdn.microsoft.com/en-us/library/y5b434w4.aspx private static readonly Dictionary> implicitNumericConversions = new Dictionary>() { { typeof(sbyte), new HashSet() { typeof(byte), typeof(int), typeof(long), typeof(float), typeof(double), typeof(decimal) } }, { typeof(byte), new HashSet() { typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(float), typeof(double), typeof(decimal) } }, { typeof(short), new HashSet() { typeof(int), typeof(long), typeof(float), typeof(double), typeof(decimal) } }, { typeof(ushort), new HashSet() { typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(float), typeof(double), typeof(decimal), } }, { typeof(int), new HashSet() { typeof(long), typeof(float), typeof(double), typeof(decimal) } }, { typeof(uint), new HashSet() { typeof(long), typeof(ulong), typeof(float), typeof(double), typeof(decimal) } }, { typeof(long), new HashSet() { typeof(float), typeof(double), typeof(decimal) } }, { typeof(char), new HashSet() { typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(float), typeof(double), typeof(decimal) } }, { typeof(float), new HashSet() { typeof(double) } }, { typeof(ulong), new HashSet() { typeof(float), typeof(double), typeof(decimal) } }, }; // https://msdn.microsoft.com/en-us/library/yht2cx7b.aspx private static readonly Dictionary> explicitNumericConversions = new Dictionary>() { { typeof(sbyte), new HashSet() { typeof(byte), typeof(ushort), typeof(uint), typeof(ulong), typeof(char) } }, { typeof(byte), new HashSet() { typeof(sbyte), typeof(char) } }, { typeof(short), new HashSet() { typeof(sbyte), typeof(byte), typeof(ushort), typeof(uint), typeof(ulong), typeof(char) } }, { typeof(ushort), new HashSet() { typeof(sbyte), typeof(byte), typeof(short), typeof(char) } }, { typeof(int), new HashSet() { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(uint), typeof(ulong), typeof(char) } }, { typeof(uint), new HashSet() { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(char) } }, { typeof(long), new HashSet() { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(ulong), typeof(char) } }, { typeof(ulong), new HashSet() { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(char) } }, { typeof(char), new HashSet() { typeof(sbyte), typeof(byte), typeof(short) } }, { typeof(float), new HashSet() { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(char), typeof(decimal) } }, { typeof(double), new HashSet() { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(char), typeof(float), typeof(decimal), } }, { typeof(decimal), new HashSet() { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(char), typeof(float), typeof(double) } } }; #endregion } }