Почему я не могу передать одно экземпляр родового типа другому?
Как я могу реализовать структуру, чтобы выполнить следующую трансляцию?
var a = new StatusedValue<double>(1, false);
var b = (StatusedValue<int>)a;
Моя реализация должна вести себя аналогично Nullable<T>
, которая отлично работает. Однако этот код не работает с System.InvalidCastException
:
public struct StatusedValue<T> where T : struct
{
public StatusedValue(T value) : this(value, true)
{
}
public StatusedValue(T value, bool isValid)
{
this.value = value;
this.isValid = isValid;
}
private T value;
private bool isValid;
public static implicit operator StatusedValue<T>(T value)
{
return new StatusedValue<T>(value);
}
public static explicit operator T(StatusedValue<T> value)
{
return value.value;
}
}
Результат:
Невозможно передать объект типа "StatusedValue`1 [System.Double]" для ввода 'StatusedValue`1 [System.Int32]'.
Ответы
Ответ 1
Это работает для типов Nullable<T>
, потому что они получают специальное лечение от компилятора. Это называется "отмененным оператором преобразования", и вы не можете определить свой собственный.
Из раздела 6.4.2 спецификация С#:
6.4.2 Поднятые операторы преобразования
С учетом пользовательского оператора преобразования, который преобразует из значения N с недействительным значением S в непустое значение типа T, существует оператор с отмененным преобразованием, который преобразует из S? к Т?. Этот оператор с отмененным преобразованием выполняет Развертывание с S? на S, за которым следует пользовательское преобразование из S к T, за которым следует обертка из T в T?, за исключением того, что нулевой S? преобразуется непосредственно к нулевому значению T?. Оператор с отмененным преобразованием имеет ту же неявную или явную классификацию, что и ее базовая пользовательский оператор преобразования. Термин "пользовательское преобразование" применяется к использованию как пользовательского, так и отмененного преобразования Операторы
Ответ 2
Если вы счастливы вызвать метод, попробуйте
public StatusedValue<U> CastValue<U>() where U : struct
{
return new StatusedValue<U>((U)Convert.ChangeType(value, typeof(U)), isValid);
}
Это, к сожалению, будет вытесняться во время выполнения, а не компилировать время, если T
не может быть преобразовано в U
.
Изменить: Как указано ниже, если вы ограничиваете IConvertible
, а также/вместо struct
, то каждое преобразование теоретически возможно во время компиляции, и вы получите только из-за неправильных значений времени выполнения.
Ответ 3
Nullables специально обрабатываются компилятором, я не знаю, так ли это здесь.
Ваши операторы позволят это:
StatusedValue<int> b = (int)a;
который, вероятно, не то, что вы хотите, потому что IsValid
не копируется таким образом.
Вы можете реализовать метод расширения следующим образом:
public static StatusedValue<TTarget> Cast<TSource, TTarget>(this StatusedValue<TSource> source)
where TTarget : struct
where TSource : struct
{
return new StatusedValue<TTarget>(
(TTarget)Convert.ChangeType(
source.Value,
typeof(TTarget)),
source.IsValid);
}
b = a.Cast<int>();
Но компилятор не может проверить, совместимы ли эти типы. ChangeType также возвращает объект, тем самым боксируя ваше значение.
Ответ 4
Если вам не нужна кастинг, вы можете добавить такой метод:
public StatusedValue<int> ConvertToStatusedInt() {
return new StatusedValue<int>(Convert.ToInt32(value), isValid);
}
Как указано в комментарии:
public StatusedValue<Q> ConvertTo<Q>() where Q:struct {
return new StatusedValue<Q>((Q)Convert.ChangeType(value, typeof(Q)), isValid);
}
Ответ 5
Для обходного пути вам необходимо предоставить способ преобразования из одного базового типа в другой, поскольку компилятор не сможет понять это:
public StatusedValue<TResult> To<TResult>(Func<T, TResult> convertFunc)
where TResult : struct {
return new StatusedValue<TResult>(convertFunc(value), isValid);
}
Затем вы можете:
var a = new StatusedValue<double>(1, false);
var b = a.To(Convert.ToInt32);
С некоторым отражением вы можете создать таблицу поиска методов Convert
и найти правильный вариант на основе аргументов типа, а затем вы можете по умолчанию преобразовать функцию null
, и если она не указана, попробуйте автоматически искать правильное преобразование. Это удалит неуклюжую часть Convert.ToInt32
и просто сделает var b = a.To<int>();
Как указывает Ролинг, Convert.ChangeType
можно использовать. Это заставит мой метод выглядеть так:
public StatusedValue<T2> To<T2>(Func<T, T2> convertFunc = null)
where T2 : struct {
return new StatusedValue<T2>(
convertFunc == null
? (T2)Convert.ChangeType(value, typeof(T2))
: convertFunc(value),
isValid
);
}
Ответ 6
Ответ на вопрос, почему это так уже был опубликован и помечен как ответ.
Тем не менее, вы можете упростить синтаксис, чтобы сделать его проще и понятнее, сохраняя при этом защиту типа времени компиляции.
Во-первых, напишите вспомогательный класс, чтобы избежать необходимости указывать параметры избыточного типа:
public static class StatusedValue
{
public static StatusedValue<T> Create<T>(T value, bool isValid = true) where T: struct
{
return new StatusedValue<T>(value, isValid);
}
}
Далее вам нужно выставить базовое значение с помощью свойства Value
(иначе вы не можете его отличить от кода).
Наконец, вы можете изменить исходный код:
var a = new StatusedValue<double>(1, false);
var b = (StatusedValue<int>)a;
Для этого:
var a = StatusedValue.Create(1.0, false);
var b = StatusedValue.Create((int)a.Value, false);
где вы делаете простой перевод на a.Value
.