Явное приведение универсального типа к другому типу в C#

У меня есть следующий код для С++ в шаблонном классе, представляющем точку. Я хотел бы перевести его на С#:

template <class T>
class Point
{
    public:
        T x;
        T y;
        T z;

    template<typename U> explicit Point(const Point<U> &p)
       : x((T)p.x), y((T)p.y), z((T)p.z)
    {
    }
}

Этот код позволяет явно преобразовать точку данного типа в точку другого типа. Например, вы можете использовать это как-то так (по общему признанию, я не на 100% уверен в синтаксисе здесь, но я понял концепцию):

Point<float> p;
Point<int> q = (Point<int>)p;

Как я могу включить эквивалент этого в С#? Пока у меня есть:

public class Point<T>
{
    public T X { get; set; }
    public T Y { get; set; }
    public T Z { get; set; }

    // Constructors exist that accept X, Y, and Z as parameters

    public static explicit operator Point<U>(Point<T> p)
    {

    }
}

Однако это дает ошибку, говоря, что «U» не определено. Это имеет смысл... но как/где мне определить U? Мой подход неверен?

Разница между моим вопросом и вопросом здесь что я просто меняю базовый тип универсального класса с помощью приведения... не пытаюсь изменить один универсальный класс на другой универсальный класс с тем же базовым типом.


person Michael Kintscher they-them    schedule 20.02.2019    source источник
comment
Операторы преобразования не могут быть универсальными.   -  person TheGeneral    schedule 20.02.2019
comment
Вам нужен U в качестве общего параметра? Вы можете заменить U фактическим типом. Конечно, у вас будет только ограниченное количество типов конверсий, которые вы можете сделать.   -  person gunr2171    schedule 20.02.2019


Ответы (5)


Я думаю, что лучшее, что вы можете получить, это:

public class Point<T>
{
    public T X { get; set; }
    public T Y { get; set; }
    public T Z { get; set; }

    public Point<U> As<U>()
    {
        return new Point<U>()
        {
            X = Convert<U>(X),
            Y = Convert<U>(Y),
            Z = Convert<U>(Z)
        };
    }

    static U Convert<U>(T t) => (U)System.Convert.ChangeType(t, typeof(U));
}

Вы не можете определить общий оператор преобразования, поэтому вам нужна явная функция. Более того, простое приведение (U)t не сработает, поэтому вам нужно Convert.ChangeType (что сработает, если ваши типы числовые).

Применение:

var p1 = new Point<int> { X = 1, Y = 2, Z = 3 };
var p2 = p1.As<double>();

(работает, как ожидалось).

person Vlad    schedule 20.02.2019
comment
Обратите внимание, что Convert.ChangeType работает только для типов, реализующих IConvertible. Что совершенно нормально, если OP придерживается примитивных типов. - person Kevin Gosse; 21.02.2019
comment
@KevinGosse: да, я говорю в своем ответе, что будет работать, если ваши типы числовые. Спасибо за подсказку! - person Vlad; 21.02.2019

Насколько мне известно, такое универсальное приведение разрешено в C# только в том случае, если между T и U существует какое-то отношение наследования.

Ближайшим эквивалентом было бы определение универсального метода для преобразования:

public Point<U> To<U>()
{
    dynamic p = this;

    return new Point<U>((U)p.X, (U)p.Y, (U)p.Z);
}

Вы не можете напрямую преобразовать T в U, так как компилятор не может знать, будет ли это безопасно. Я использую ключевое слово dynamic, чтобы обойти это ограничение.

person Kevin Gosse    schedule 20.02.2019
comment
Тогда это кажется более общим, поскольку для него не потребуются числовые типы. Моя проблема возникает (будут использоваться только числовые типы)… но это полезно знать на будущее, если это не так. Мне интересно, в чем разница в производительности между этим и вариантом Convert.ChangeType. - person Michael Kintscher they-them; 21.02.2019
comment
@MichaelKintscher Хороший вопрос. Я провел быстрый тест, я получил 127 нс для Convert.ChangeType против 53 нс для Dynamic. Обратите внимание, что первый вызов dynamic намного дороже (поскольку выражение оценивается во время первого вызова, а затем кэшируется). - person Kevin Gosse; 21.02.2019
comment
Хорошо знать! Я также добавил where U : IConvertible в качестве ограничения как для T, так и для U, просто чтобы быть безопасным для потребителя моего класса. - person Michael Kintscher they-them; 21.02.2019

Подобно ответу Кевина, но без dynamic нужно использовать двойное приведение:

public Point<U> To<U>()
{
    return new Point<U>((U)(object)X, (U)(object)Y, (U)(object)Z);
}

Оба наших ответа не обнаруживают никаких проблем во время компиляции.

person Kit    schedule 20.02.2019
comment
Это не сработает, вам нужно распаковать значение перед его приведением. int i = (int)(object)1.0f;выбрасывает InvalidCastException - person Kevin Gosse; 21.02.2019
comment
Хм. О да, ты прав. Я использую двойное приведение во многих универсальных проектах, но это всегда было для ссылочных типов. - person Kit; 21.02.2019

Вы не можете объявить операторы с дополнительными аргументами универсального типа, но вы можете объявить их для или из определенных универсальных типов, таких как Point<int>. C# также не позволит вам выполнять произвольные преобразования путем приведения от или к T.

Наименее шаблонным вариантом, который поддерживает хоть какую-то безопасность типов, будет ограничение параметра T значением IConvertible:

public class Point<T> where T : IConvertible
{
    // ...

    public static explicit operator Point<int>(Point<T> point)
    {
        // The IFormatProvider parameter has no effect on purely numeric conversions
        return new Point<int>(point.X.ToInt32(null), point.Y.ToInt32(null), point.Y.ToInt32(null));
    }    
}

Однако это не помешает пользователям объявлять бессмысленные, неподдерживаемые типы, такие как Point<DateTime>, которые затем будут выбрасываться во время выполнения при попытке преобразования.

person kalimag    schedule 20.02.2019

Вы не можете определить дополнительные ограничения универсального типа, но вы можете сделать что-то подобное, используя операторы и методы.

public class Point<T>
{
    public T X { get; set; }
    public T Y { get; set; }
    public T Z { get; set; }

    public static explicit operator Point<T>(Point<int> v)
    {
        return v.As<T>();
    }

    public static explicit operator Point<T>(Point<double> v)
    {
        return v.As<T>();
    }

    public static explicit operator Point<T>(Point<float> v)
    {
        return v.As<T>();
    }

    public Point<TU> As<TU>()
    {
        return new Point<TU>()
        {
            X = ConvertTo<TU>(X),
            Y = ConvertTo<TU>(Y),
            Z = ConvertTo<TU>(Z)
        };
    }

    private static TU ConvertTo<TU>(T t)
    {
        return (TU) Convert.ChangeType(t, typeof(TU));
    }
}

Применение:

        Point<double> d = new Point<double>()
        {
            X = 10d, Y = 10d, Z = 10d
        };

        Point<int> i = (Point<int>) d;

        Point<float> f = (Point<float>) i;

        d = (Point<double>) f;
person Vahid    schedule 20.02.2019