简体   繁体   中英

Why can't I cast one instantiation of a generic type to another?

How can I implement a struct so that the following cast can be performed?

var a = new StatusedValue<double>(1, false);
var b = (StatusedValue<int>)a;

My implementation should behave similarly to Nullable<T> , which works fine. However, this code fails with 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;
    }
}

Result:

Unable to cast object of type 'StatusedValue`1[System.Double]' to type 'StatusedValue`1[System.Int32]'.

This works for Nullable<T> types because they get special treatment from the compiler. This is called a "lifted conversion operator", and you cannot define your own.

From section 6.4.2 of the C# Specification :

6.4.2 Lifted conversion operators

Given a user-defined conversion operator that converts from a non-nullable value type S to a non-nullable value type T, a lifted conversion operator exists that converts from S? to T?. This lifted conversion operator performs an unwrapping from S? to S followed by the user-defined conversion from S to T followed by a wrapping from T to T?, except that a null valued S? converts directly to a null valued T?. A lifted conversion operator has the same implicit or explicit classification as its underlying user-defined conversion operator. The term “user-defined conversion” applies to the use of both user-defined and lifted conversion operators

If you're happy calling a method, try

public StatusedValue<U> CastValue<U>() where U : struct
{
    return new StatusedValue<U>((U)Convert.ChangeType(value, typeof(U)), isValid);
}

This will unfortunately throw at runtime rather than compile time if T cannot be converted to U .

Edit: As pointed out below, if you constrain to IConvertible as well as/instead of struct then every conversion is theoretically possible at compile time, and you'll only get a runtime failure because of bad runtime values.

Nullables are specially handled by the compiler, I don't know if this is the case here.

Your operators would allow this:

StatusedValue<int> b = (int)a;

which is probably not what you want, because IsValid is not copied this way.

You could implement an extension method like this:

 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>();

But the compiler cannot check if the types are compatible. ChangeType also returns an object, thus boxing your value.

If you don't need a casting, you can add a method like this:

    public StatusedValue<int> ConvertToStatusedInt() {
        return new StatusedValue<int>(Convert.ToInt32(value), isValid);
    }

As suggested in comment:

    public StatusedValue<Q> ConvertTo<Q>() where Q:struct {
        return new StatusedValue<Q>((Q)Convert.ChangeType(value, typeof(Q)), isValid);
    }

For a workaround, you will need to provide a way for converting from one underlying type to the other, since the compiler won't be able to figure that out:

public StatusedValue<TResult> To<TResult>(Func<T, TResult> convertFunc)
where TResult : struct {
   return new StatusedValue<TResult>(convertFunc(value), isValid);
}

You can then do:

var a = new StatusedValue<double>(1, false);
var b = a.To(Convert.ToInt32);

With some reflection you could build a lookup table of the Convert methods, and lookup the right one based on the type arguments, and then you could default the conversion function to null and if it's not provided, try to lookup the correct conversion automatically. This would remove the clumsy Convert.ToInt32 part, and simply do var b = a.To<int>();

As Rawling points out, Convert.ChangeType can be used. This would make my method look like:

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
   );
}

The answer to why it's like this has already been posted and marked as the answer.

However, you can simplify the syntax to make it easier and clearer to do this while retaining compile-time type-safety.

Firstly, write a helper class to avoid having to specify redundant type parameters:

public static class StatusedValue
{
    public static StatusedValue<T> Create<T>(T value, bool isValid = true) where T: struct
    {
        return new StatusedValue<T>(value, isValid);
    }
}

Next you need to expose the underlying value with a Value property (otherwise you can't cast it from code).

Finally you can change your original code from this:

var a = new StatusedValue<double>(1, false);
var b = (StatusedValue<int>)a;

To this:

var a = StatusedValue.Create(1.0, false);
var b = StatusedValue.Create((int)a.Value, false); 

where you are doing a simple cast on a.Value .

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM