Как определить, является ли тип A неявно конвертируемым в тип B
Учитывая Тип a и Тип b, как я могу во время выполнения определить, существует ли неявное преобразование из a в b?
Если это не имеет смысла, рассмотрите следующий метод:
public PropertyInfo GetCompatibleProperty<T>(object instance, string propertyName)
{
var property = instance.GetType().GetProperty(propertyName);
bool isCompatibleProperty = !property.PropertyType.IsAssignableFrom(typeof(T));
if (!isCompatibleProperty) throw new Exception("OH NOES!!!");
return property;
}
И вот код вызова, который я хочу работать:
// Since string.Length is an int property, and ints are convertible
// to double, this should work, but it doesn't. :-(
var property = GetCompatibleProperty<double>("someStringHere", "Length");
5 ответов
Обратите внимание, что IsAssignableFrom
НЕ решает вашу проблему. Вы должны использовать Reflection, как так. Обратите внимание на явную необходимость обработки примитивных типов; эти списки в соответствии с §6.1.2 (Неявные числовые преобразования) спецификации.
static class TypeExtensions {
static Dictionary<Type, List<Type>> dict = new Dictionary<Type, List<Type>>() {
{ typeof(decimal), new List<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(char) } },
{ typeof(double), new List<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(char), typeof(float) } },
{ typeof(float), new List<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(char), typeof(float) } },
{ typeof(ulong), new List<Type> { typeof(byte), typeof(ushort), typeof(uint), typeof(char) } },
{ typeof(long), new List<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(char) } },
{ typeof(uint), new List<Type> { typeof(byte), typeof(ushort), typeof(char) } },
{ typeof(int), new List<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(char) } },
{ typeof(ushort), new List<Type> { typeof(byte), typeof(char) } },
{ typeof(short), new List<Type> { typeof(byte) } }
};
public static bool IsCastableTo(this Type from, Type to) {
if (to.IsAssignableFrom(from)) {
return true;
}
if (dict.ContainsKey(to) && dict[to].Contains(from)) {
return true;
}
bool castable = from.GetMethods(BindingFlags.Public | BindingFlags.Static)
.Any(
m => m.ReturnType == to &&
(m.Name == "op_Implicit" ||
m.Name == "op_Explicit")
);
return castable;
}
}
Использование:
bool b = typeof(A).IsCastableTo(typeof(B));
Принятый ответ на этот вопрос касается многих случаев, но не всех. Например, вот только несколько допустимых приведений / преобразований, которые не обрабатываются правильно:
// explicit
var a = (byte)2;
var b = (decimal?)2M;
// implicit
double? c = (byte)2;
decimal? d = 4L;
Ниже я разместил альтернативную версию этой функции, которая конкретно отвечает на вопрос приведения и преобразования IMPLICIT. Для получения более подробной информации, комплект тестов, который я использовал для проверки, и версию EXPLICIT, пожалуйста, ознакомьтесь с моим постом на эту тему.
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
if (to.IsAssignableFrom(from))
{
return true;
}
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]);
return true;
}
catch (TargetInvocationException ex)
{
return = !(
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$")
);
}
}
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 = Microsoft.CSharp.RuntimeBinder.Binder.InvokeMember(
flags: CSharpBinderFlags.ResultDiscarded,
name: "Add",
typeArguments: null,
context: typeof(TypeHelpers), // the current type
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));
}
Неявные преобразования, которые вы должны учитывать:
- тождественность
- sbyte to short, int, long, float, double или decimal
- byte to short, ushort, int, uint, long, ulong, float, double или decimal
- short to int, long, float, double или decimal
- ushort to int, uint, long, ulong, float, double или decimal
- int to long, float, double или decimal
- От Uint до Long, Ulong, Float, двойной или десятичной
- долго плавать, двойной или десятичный
- ulong to float, double или decimal
- char to ushort, int, uint, long, ulong, float, double или decimal
- плавать, чтобы удвоить
- Обнуляемое преобразование типов
- Тип ссылки на объект
- Производный класс в базовый класс
- Класс для реализованного интерфейса
- Интерфейс к базовому интерфейсу
- Массив в массив, если у массивов одинаковое количество измерений, существует неявное преобразование из типа исходного элемента в тип элемента назначения, а тип исходного элемента и тип элемента назначения являются ссылочными типами
- Тип массива в System.Array
- Тип массива в IList<> и его базовые интерфейсы
- Тип делегата для System.Delegate
- Конвертация бокса
- Тип перечисления в System.Enum
- Определяемое пользователем преобразование (op_implicit)
Я полагаю, вы ищете последнее. Вам нужно написать что-то похожее на компилятор, чтобы охватить все из них. Примечательно, что System.Linq.Expressions.Expression не пытался выполнить этот подвиг.
Вот метод, который проходит все следующие тесты:
[Test] public void TestImplicitlyCastable()
{
Assert.That( typeof(byte) .IsImplicitlyCastableTo(typeof(short)));
Assert.That( typeof(byte) .IsImplicitlyCastableTo(typeof(byte?)));
Assert.That( typeof(byte) .IsImplicitlyCastableTo(typeof(long?)));
Assert.That(!typeof(short) .IsImplicitlyCastableTo(typeof(uint)));
Assert.That( typeof(long) .IsImplicitlyCastableTo(typeof(float)));
Assert.That( typeof(long) .IsImplicitlyCastableTo(typeof(decimal)));
Assert.That(!typeof(double) .IsImplicitlyCastableTo(typeof(decimal)));
Assert.That(!typeof(decimal) .IsImplicitlyCastableTo(typeof(double)));
Assert.That( typeof(List<int>).IsImplicitlyCastableTo(typeof(object)));
Assert.That( typeof(float) .IsImplicitlyCastableTo(typeof(IComparable<float>)));
Assert.That( typeof(long?) .IsImplicitlyCastableTo(typeof(IComparable<long>)));
Assert.That(!typeof(object) .IsImplicitlyCastableTo(typeof(string)));
Assert.That( typeof(string[]).IsImplicitlyCastableTo(typeof(object[])));
Assert.That( typeof(Foo) .IsImplicitlyCastableTo(typeof(int)));
Assert.That(!typeof(Foo) .IsImplicitlyCastableTo(typeof(uint)));
Assert.That( typeof(Foo) .IsImplicitlyCastableTo(typeof(long)));
Assert.That( typeof(Foo) .IsImplicitlyCastableTo(typeof(long?)));
}
class Foo
{
public static implicit operator int(Foo f) => 42;
}
Он основан на уловке с dynamic
вдохновленный ответом ChaseMedallion:
using System;
using System.Collections.Generic;
using System.Linq.Expressions;
using System.Reflection;
using Microsoft.CSharp.RuntimeBinder;
public static class ReflectionHelpers
{
[ThreadStatic]
static readonly Dictionary<KeyValuePair<Type, Type>, bool> ImplicitCastCache;
/// <summary>Returns true iff casting between values of the specified
/// types is possible based on the rules of C#.</summary>
public static bool IsImplicitlyCastableTo(this Type from, Type to)
{
if (from == to)
return true;
var key = new KeyValuePair<Type, Type>(from, to);
ImplicitCastCache ??= new Dictionary<KeyValuePair<Type, Type>, bool>();
if (ImplicitCastCache.TryGetValue(key, out bool result))
return result;
if (to.IsAssignableFrom(from))
return ImplicitCastCache[key] = true;
var method = GetMethodInfo(() => IsImplicitlyCastableCore<int, int>())
.GetGenericMethodDefinition().MakeGenericMethod(from, to);
return ImplicitCastCache[key] = (bool)method.Invoke(null, Array.Empty<object>());
}
static bool IsImplicitlyCastableCore<TFrom,TTo>()
{
var testObject = new LinkedListNode<TTo>(default(TTo));
try {
((dynamic)testObject).Value = default(TFrom);
return true;
} catch (Exception e) {
// e.g. "Cannot implicitly convert type 'A' to 'B'. An explicit conversion exists (are you missing a cast?)"
// The exception may be caused either because no conversion is available,
// OR because it IS available but the conversion method threw something.
// Assume RuntimeBinderException means the conversion does not exist.
return !(e is RuntimeBinderException);
}
}
/// <summary><c>GetMethodInfo(() => M(args))</c> gets the MethodInfo object corresponding to M.</summary>
public static MethodInfo GetMethodInfo(Expression<Action> shape) => ((MethodCallExpression)shape.Body).Method;
}
Хотя ответ Джейсона - хорошее начало, он не распространяется на нулевые значения. Мне нужно было немного более общее решение, которое охватывало бы значения NULL и, учитывая два типа, нашло бы наилучший общий тип, включая неявные и явные преобразования.
(Если вам нужны только неявные преобразования, нижеприведенное, вероятно, может быть изменено для них).
/// <summary>Finds the best common type among the given types.</summary>
/// <param name="type1">The first type to check.</param>
/// <param name="type2">The second type to check.</param>
/// <returns>The best common type.</returns>
public static Type FindBestCommonType(Type type1, Type type2)
{
if (type1 == null && type2 == null) throw new ArgumentNullException("One of the two types must be non-null.");
if (type1 == null) return ensureNullable(type2);
if (type2 == null) return ensureNullable(type1);
if (type1 == type2) return type1;
if (type1.IsAssignableFrom(type2)) return type1;
if (type2.IsAssignableFrom(type1)) return type2;
Type bestCommonType = null;
var type1Underlying = Nullable.GetUnderlyingType(type1);
var type2Underlying = Nullable.GetUnderlyingType(type2);
var type1Nullable = type1Underlying != null;
var type2Nullable = type2Underlying != null;
var resultMustBeNullable = type1Nullable || type2Nullable;
type1 = type1Underlying ?? type1;
type2 = type2Underlying ?? type2;
//If our nullable-stripped types are equivalent, send back the nullable version
if (type1 == type2)
return typeof(Nullable<>).MakeGenericType(type1);
var type1Convertibles = _convertibleTypes.GetValueOrDefault(type1);
var type2Convertibles = _convertibleTypes.GetValueOrDefault(type2);
bestCommonType = type1Convertibles?.Contains(type2) == true ? type1
: type2Convertibles?.Contains(type1) == true ? type2
: null;
//Check for implicit or explicit conversion
if (bestCommonType == null)
{
if (type1.GetMethods(BindingFlags.Public | BindingFlags.Static)
.Any(m => m.ReturnType == type2 && (m.Name == "op_Implicit" || m.Name == "op_Explicit")))
bestCommonType = type2;
else if (type2.GetMethods(BindingFlags.Public | BindingFlags.Static)
.Any(m => m.ReturnType == type1 && (m.Name == "op_Implicit" || m.Name == "op_Explicit")))
bestCommonType = type1;
}
if (resultMustBeNullable && bestCommonType != null && bestCommonType != typeof(object))
bestCommonType = typeof(Nullable<>).MakeGenericType(bestCommonType);
return bestCommonType ?? typeof(object);
//Function to ensure that the given type can hold nulls - if its a reference type it does nothing - if it's a value type it ensures it is Nullable<T>
static Type ensureNullable(Type t) => t.IsValueType && Nullable.GetUnderlyingType(t) == null ? typeof(Nullable<>).MakeGenericType(t) : t;
}
private static readonly Dictionary<Type, HashSet<Type>> _convertibleTypes = new Dictionary<Type, HashSet<Type>>() {
{ typeof(decimal), new HashSet<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(char) } },
{ typeof(double), new HashSet<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(char), typeof(float) } },
{ typeof(float), new HashSet<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(long), typeof(ulong), typeof(char) } },
{ typeof(ulong), new HashSet<Type> { typeof(byte), typeof(ushort), typeof(uint), typeof(char) } },
{ typeof(long), new HashSet<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(int), typeof(uint), typeof(char) } },
{ typeof(uint), new HashSet<Type> { typeof(byte), typeof(ushort), typeof(char) } },
{ typeof(int), new HashSet<Type> { typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(char) } },
{ typeof(ushort), new HashSet<Type> { typeof(byte), typeof(char) } },
{ typeof(short), new HashSet<Type> { typeof(byte) } }
};
}
Его можно перегрузить, чтобы найти наилучший общий тип для произвольно большого набора типов:
/// <summary>Finds the best common type among the given types.</summary>
/// <param name="types">The types to check.</param>
/// <returns>The best common type.</returns>
public static Type FindBestCommonType(params Type[] types)
{
if (types == null) throw new ArgumentNullException(nameof(types));
var filteredTypes = types.Distinct().ToList();
if (filteredTypes.Count == 0) throw new InvalidOperationException("No types were provided");
var bestCommonType = filteredTypes[0];
foreach (var type in filteredTypes.Skip(1))
{
bestCommonType = FindBestCommonType(type, bestCommonType);
}
return bestCommonType;
}
Затем мы можем использовать это для расширения неизвестных типов во время выполнения:
/// <summary>
/// Attempts to widen the given objects so that they are both compatible types.
/// </summary>
/// <param name="o1">The first object, passed by reference.</param>
/// <param name="o2">The second object, passed by reference.</param>
public static void WidenToEqualTypes(ref object o1, ref object o2)
{
var type1 = o1.GetType();
var type2 = o2.GetType();
var bestCommonType = FindBestCommonType(type1, type2);
o1 = Convert.ChangeType(o1, bestCommonType);
o2 = Convert.ChangeType(o2, bestCommonType);
}
Модульные тесты:
[TestCase(typeof(long), new[] { typeof(int), typeof(long) })]
[TestCase(typeof(long?), new[] {typeof(int), typeof(long?)})]
[TestCase(typeof(long?), new[] {typeof(int?), typeof(long)})]
[TestCase(typeof(double?), new[] {typeof(int?), typeof(double)})]
[TestCase(typeof(decimal), new[] {typeof(long), typeof(decimal)})]
[TestCase(typeof(double), new[] { typeof(float), typeof(double) })]
[TestCase(typeof(bool?), new[] {typeof(bool?), typeof(bool)})]
[TestCase(typeof(bool?), new[] { null, typeof(bool) })]
[TestCase(typeof(string), new[] { typeof(string), null })]
[TestCase(typeof(DateTime), new[] {typeof(DateOnly), typeof(DateTime)})]
[TestCase(typeof(DateTime?), new[] {typeof(DateOnly?), typeof(DateTime)})]
[TestCase(typeof(DateTime?), new[] {typeof(DateTime?), typeof(DateOnly)})]
[TestCase(typeof(object), new[] {typeof(string), typeof(int)})]
[TestCase(typeof(Guid?), new[] {typeof(Guid), typeof(Guid?)})]
[TestCase(typeof(double?), new[] { typeof(int), typeof(long?), typeof(double) })]
[TestCase(typeof(DateTime?), new[] { typeof(DateTime), typeof(DateOnly?), typeof(DateOnly) })]
[TestCase(typeof(IEnumerable<int>), new[] { typeof(IEnumerable<int>), typeof(List<int>) })]
[Description("Finds the best common type that all the supplied types are convertible to.")]
public void BestCommonTypesTests(Type expected, Type[] types)
{
Assert.AreEqual(expected, Util.FindBestCommonType(types));
}