Чи існує обмеження, яке обмежує мій загальний метод числовими типами?


364

Хто-небудь може сказати мені, чи є спосіб із дженериками обмежити аргумент загального типу Tлише:

  • Int16
  • Int32
  • Int64
  • UInt16
  • UInt32
  • UInt64

Мені відомо whereключове слово, але не можу знайти інтерфейс лише для цих типів,

Щось на зразок:

static bool IntegerFunction<T>(T value) where T : INumeric 

Відповіді:


140

C # це не підтримує. У інтерв'ю Брюсу Еккелю Хейльсберг описав причини невтілення цієї функції :

І не ясно, що додаткова складність варта того невеликого врожаю, який ви отримаєте. Якщо щось, що ви хочете зробити, не підтримується безпосередньо в системі обмежень, ви можете зробити це з заводською схемою. Ви могли б мати Matrix<T>, наприклад, і в тому , що Matrixви хотіли б визначити метод скалярного твори. Це, звичайно, означає , що ви в кінцевому рахунку , необхідно зрозуміти , як помножити два Ts, але ви не можете сказати , що в якості обмеження, по крайней мере, якщо Tє int, doubleабо float. Але те, що ти можеш зробити, - це Matrixвзяти аргумент a Calculator<T>, і в Calculator<T>, створити метод, який називається multiply. Ви переходите до цього, і передаєте його Matrix.

Однак це призводить до досить складного коду, коли користувач повинен подати власну Calculator<T>реалізацію для кожного, Tякий він хоче використовувати. До тих пір, поки це не повинно бути розширюваним, тобто якщо ви просто хочете підтримувати фіксовану кількість типів, таких як, intі double, ви можете піти з відносно простим інтерфейсом:

var mat = new Matrix<int>(w, h);

( Мінімальна реалізація в GitHub Gist. )

Однак, як тільки ви бажаєте, щоб користувач міг надавати власні, власні типи, вам потрібно відкрити цю реалізацію, щоб користувач міг надавати власні Calculatorекземпляри. Наприклад, щоб створити матрицю, яка використовує власну десяткову реалізацію з плаваючою комою DFP, вам доведеться написати цей код:

var mat = new Matrix<DFP>(DfpCalculator.Instance, w, h);

… Та впроваджувати всіх членів для DfpCalculator : ICalculator<DFP>.

Альтернативою, яка, на жаль, має однакові обмеження, є робота з заняттями з політики, про що йшлося у відповіді Сергія Шандара .


25
btw, MiscUtil надає загальний клас, який робить саме це; Operator/ Operator<T>; yoda.arachsys.com/csharp/miscutil/usage/genericoperators.html
Marc Gravell

1
@Mark: хороший коментар. Однак для того, щоб бути зрозумілим, я не думаю, що Хейльсберг мав на увазі генерацію коду як рішення проблеми, як це робиться в Operator<T>кодексі (оскільки інтерв'ю було дано задовго до існування Expressionsоснови, навіть якщо можна було звичайно використовуйте Reflection.Emit) - і я був би дуже зацікавлений у його вирішенні.
Конрад Рудольф

@Konrad Rudolph: Я думаю, що ця відповідь на подібне питання пояснює вирішення Хейльсберга. Інший родовий клас робиться абстрактним. Оскільки він вимагає від вас реалізувати інший загальний клас для кожного типу, який ви хочете підтримати, це призведе до повторення коду, але це означає, що ви можете створити інстанціювання лише оригінального загального класу з підтримуваним типом.
Ergwun

14
Я не погоджуюся з фразою Хейсберга "Отже, в певному сенсі шаблони C ++ насправді не типізовані або друковані. Враховуючи, що C # генерики сильно набрані". Це дійсно маркетинг BS для просування C #. Сильний / слабкий типізація не пов'язаний з якістю діагностики. Інакше: Цікава знахідка.
Себастьян Мах

100

З огляду на популярність цього питання та зацікавленість такою функцією, я здивований, коли відповіді, що стосується Т4, ще немає.

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

Замість того, щоб переглядати обручі і жертвувати визначеністю часу компіляції, ви можете просто генерувати потрібну функцію для кожного типу, який вам подобається, і використовувати його відповідно (під час компіляції!).

Для цього:

  • Створіть новий файл текстового шаблону під назвою GenericNumberMethodTemplate.tt .
  • Видаліть автоматично згенерований код (ви збережете його більшу частину, але деякі не потрібні).
  • Додайте такий фрагмент:
<#@ template language="C#" #>
<#@ output extension=".cs" #>
<#@ assembly name="System.Core" #>

<# Type[] types = new[] {
    typeof(Int16), typeof(Int32), typeof(Int64),
    typeof(UInt16), typeof(UInt32), typeof(UInt64)
    };
#>

using System;
public static class MaxMath {
    <# foreach (var type in types) { 
    #>
        public static <#= type.Name #> Max (<#= type.Name #> val1, <#= type.Name #> val2) {
            return val1 > val2 ? val1 : val2;
        }
    <#
    } #>
}

Це воно. Ви закінчили зараз.

Збереження цього файлу автоматично скомпілює його у цей вихідний файл:

using System;
public static class MaxMath {
    public static Int16 Max (Int16 val1, Int16 val2) {
        return val1 > val2 ? val1 : val2;
    }
    public static Int32 Max (Int32 val1, Int32 val2) {
        return val1 > val2 ? val1 : val2;
    }
    public static Int64 Max (Int64 val1, Int64 val2) {
        return val1 > val2 ? val1 : val2;
    }
    public static UInt16 Max (UInt16 val1, UInt16 val2) {
        return val1 > val2 ? val1 : val2;
    }
    public static UInt32 Max (UInt32 val1, UInt32 val2) {
        return val1 > val2 ? val1 : val2;
    }
    public static UInt64 Max (UInt64 val1, UInt64 val2) {
        return val1 > val2 ? val1 : val2;
    }
}

У вашому mainметоді ви можете переконатися, що у вас є визначеність часу компіляції:

namespace TTTTTest
{
    class Program
    {
        static void Main(string[] args)
        {
            long val1 = 5L;
            long val2 = 10L;
            Console.WriteLine(MaxMath.Max(val1, val2));
            Console.Read();
        }
    }
}

введіть тут опис зображення

Я випереджу одне зауваження: ні, це не є порушенням принципу DRY. Принцип DRY існує для того, щоб люди не могли дублювати код у кількох місцях, що призведе до того, що додаток стає важким для обслуговування.

Тут це зовсім не так: якщо ви хочете зміни, ви можете просто змінити шаблон (єдине джерело для всіх ваших поколінь!) І це зроблено.

Для того, щоб використовувати його з власними спеціальними визначеннями, додайте декларацію простору імен (переконайтесь, що вона є такою ж, як та, де ви визначите свою власну реалізацію) до створеного коду та позначте клас як partial. Потім додайте ці рядки до файлу шаблону, щоб він був включений до можливої ​​компіляції:

<#@ import namespace="TheNameSpaceYouWillUse" #>
<#@ assembly name="$(TargetPath)" #>

Будемо чесними: це досить круто.

Відмова від відповідальності: на цей зразок сильно вплинули метапрограмування в .NET, Кевін Хаззард та Джейсон Бок, Manning Publications .


Це досить круто, але чи можна було б змінити це рішення, щоб методи прийняли якийсь загальний тип, Tякий є або успадковується від різних IntXкласів? Мені подобається це рішення, тому що це економить час, але для нього на 100% вирішити проблему (незважаючи на те, що не так приємно, як якщо б у C # була підтримка такого типу обмежень, вбудована), кожен із створених методів все ж повинен бути загальним, так що вони можуть повернути об’єкт типу, який успадковується від одного з IntXXкласів.
Захарій Кнібель

1
@ZacharyKniebel: IntXXтипи - це структури, що означає, що вони не підтримують спадкування в першу чергу . І навіть якщо б це було, тоді застосовується принцип заміни Ліскова (який ви можете знати з ідіоми SOLID): якщо метод визначений як Xі Yє дочірнім, Xто за визначенням будь-який Yповинен бути спроможний перейти до цього методу як заміна її базовий тип.
Jeroen Vannevel

1
Це вирішення за допомогою політики stackoverflow.com/questions/32664/… використовує T4 для генерації класів.
Сергій Шандар

2
+1 для цього рішення, оскільки воно зберігає ефективність роботи вбудованих інтегральних типів, на відміну від рішень, заснованих на політиці. Виклик вбудованих операторів CLR (наприклад, Add) за допомогою додаткового (можливо, віртуального) методу може сильно вплинути на продуктивність, якщо його використовувати багато разів (наприклад, у математичних бібліотеках). А оскільки кількість інтегральних типів є постійною (і її не можна успадкувати), вам потрібно лише відновити код для виправлення помилок.
Аттіла Кленік

1
Дуже круто, і я збирався почати його використовувати, тоді я згадав, наскільки я залежний від Resharper, який я рефакторинг, і ви не можете перейменувати рефактор через шаблон T4. Це не критично, але варто задуматися.
bradgonesurfing

86

Для цього немає жодних обмежень. Це справжнє питання для тих, хто хоче використовувати дженерики для числових обчислень.

Я б пішов далі і сказав, що нам потрібно

static bool GenericFunction<T>(T value) 
    where T : operators( +, -, /, * )

Або навіть

static bool GenericFunction<T>(T value) 
    where T : Add, Subtract

На жаль, у вас є лише інтерфейси, базові класи та ключові слова struct(повинні бути типовими значеннями), class(повинні бути посиланнями) таnew() типовими (повинен мати конструктор за замовчуванням)

Ви можете загорнути номер у щось інше (схоже на INullable<T>), як тут, у кодовому проекті .


Ви можете застосувати обмеження під час виконання (відбиваючись на операторах або перевіряючи типи), але це втрачає перевагу в тому, що в першу чергу є загальне.


2
Цікаво, чи бачили Ви підтримку MiscUtil для загальних операторів ... yoda.arachsys.com/csharp/miscutil/usage/genericoperators.html
Marc Gravell

10
Так - Джон Скіт ще деякий час тому вказував мені на них (але після цьогорічної відповіді) - вони розумна ідея, але я все одно хотів би належної підтримки.
Кіт

1
Зачекайте, where T : operators( +, -, /, * )чи законний C #? Вибачте за запитання новачка.
kdbanman

@kdbanman Я так не думаю. Кіт говорить, що C # не підтримує те, що просить ОП, і пропонує нам зробити це where T : operators( +, -, /, * ), але не можемо.
AMTerp

62

Обхід використання політики:

interface INumericPolicy<T>
{
    T Zero();
    T Add(T a, T b);
    // add more functions here, such as multiplication etc.
}

struct NumericPolicies:
    INumericPolicy<int>,
    INumericPolicy<long>
    // add more INumericPolicy<> for different numeric types.
{
    int INumericPolicy<int>.Zero() { return 0; }
    long INumericPolicy<long>.Zero() { return 0; }
    int INumericPolicy<int>.Add(int a, int b) { return a + b; }
    long INumericPolicy<long>.Add(long a, long b) { return a + b; }
    // implement all functions from INumericPolicy<> interfaces.

    public static NumericPolicies Instance = new NumericPolicies();
}

Алгоритми:

static class Algorithms
{
    public static T Sum<P, T>(this P p, params T[] a)
        where P: INumericPolicy<T>
    {
        var r = p.Zero();
        foreach(var i in a)
        {
            r = p.Add(r, i);
        }
        return r;
    }

}

Використання:

int i = NumericPolicies.Instance.Sum(1, 2, 3, 4, 5);
long l = NumericPolicies.Instance.Sum(1L, 2, 3, 4, 5);
NumericPolicies.Instance.Sum("www", "") // compile-time error.

Рішення безпечно для компіляції. CityLizard Framework пропонує компільовану версію для .NET 4.0. Файл lib / NETFramework4.0 / CityLizard.Policy.dll.

Він також доступний у Nuget: https://www.nuget.org/packages/CityLizard/ . Дивіться структуру CityLizard.Policy.I .


У мене виникли проблеми з цією схемою, коли аргументів функції менше, ніж загальних параметрів. Відкрито stackoverflow.com/questions/36048248 / ...
xvan

будь-яка причина, чому використовувати struct? що робити, якщо я замість цього використовую singleton-class і змінюю екземпляр, public static NumericPolicies Instance = new NumericPolicies();а потім додаю цей конструктор private NumericPolicies() { }.
М.казем Ахгарі

@ M.kazemAkhgary ви можете використовувати одиночку. Я волію структуру. Теоретично його можна оптимізувати компілятором / CLR, оскільки структура не містить інформації. У випадку одиночного ви все одно передасте посилання, що може додати додатковий тиск на GC. Ще одна перевага полягає в тому, що структура не може бути нульовою :-).
Сергій Шандар

Я збирався сказати, що ви знайшли дуже розумне рішення, але рішення для мене занадто обмежене: я збирався його використовувати T Add<T> (T t1, T t2), але Sum()працює лише тоді, коли він може отримати власний тип T з його параметрів, що неможливо коли він вбудований в іншу загальну функцію.
Тобіас Кнаус

16

Це запитання трохи задає поширені запитання, тому я публікую це як вікі (оскільки я раніше розміщував подібні, але це старіші); все одно ...

Яку версію .NET ви використовуєте? Якщо ви використовуєте .NET 3.5, я маю реалізацію загальних операторів у MiscUtil (безкоштовно тощо).

Це методи, як T Add<T>(T x, T y)і інші варіанти для арифметики на різних типах (наприклад,DateTime + TimeSpan ).

Крім того, це працює для всіх вбудованих, піднятих та замовлених операторів, а також кешує делегата для продуктивності.

Ось додаткова інформація про те, чому це складно, є тут .

Можливо, ви також хочете знати, що dynamic(4.0) різновид вирішує це питання також опосередковано - тобто

dynamic x = ..., y = ...
dynamic result = x + y; // does what you expect

14

На жаль, ви можете лише вказати структуру в пункті де в цьому випадку. Це здається дивним, що ви не можете конкретно вказати Int16, Int32 і т.д., але я впевнений, що існує деяка причина глибокої реалізації, яка лежить в основі рішення не дозволяти типи значень у пункті, де.

Я здогадуюсь, єдине рішення - це зробити перевірку виконання, яка, на жаль, запобігає виявленню проблеми під час компіляції. Це піде на кшталт: -

static bool IntegerFunction<T>(T value) where T : struct {
  if (typeof(T) != typeof(Int16)  &&
      typeof(T) != typeof(Int32)  &&
      typeof(T) != typeof(Int64)  &&
      typeof(T) != typeof(UInt16) &&
      typeof(T) != typeof(UInt32) &&
      typeof(T) != typeof(UInt64)) {
    throw new ArgumentException(
      string.Format("Type '{0}' is not valid.", typeof(T).ToString()));
  }

  // Rest of code...
}

Що я трохи некрасиво знаю, але принаймні забезпечує необхідні обмеження.

Я також розглядаю можливі наслідки для цієї реалізації, можливо, там є швидший шлях.


13
+1, однак // Rest of code...може не компілюватися, якщо це залежить від операцій, визначених обмеженнями.
Нік

1
Convert.ToIntXX (значення) може допомогти скласти компіляцію "// Решта коду" - принаймні, поки тип повернення IntegerFunction також не буде типу T, тоді ви перебуваєте з обручем. :-p
yoyo

-1; це не працює з тієї причини, яку вказав @Nick. У момент, коли ви намагаєтесь робити які-небудь арифметичні операції, // Rest of code...як-от value + valueабо value * value, ви отримаєте помилку компіляції.
Марк Амері

13

Напевно, це найближче, що ти можеш зробити

static bool IntegerFunction<T>(T value) where T: struct

Не впевнений, чи можете ви зробити наступне

static bool IntegerFunction<T>(T value) where T: struct, IComparable
, IFormattable, IConvertible, IComparable<T>, IEquatable<T>

Для чогось такого специфічного, чому б просто не було перевантажень для кожного типу, список настільки короткий, і, можливо, він матиме менший слід пам'яті.


6

Починаючи з C # 7.3, ви можете скористатися ближчим наближенням - некерованим обмеженням, щоб вказати, що параметр типу - це не вказівний, нерегульований некерований тип.

class SomeGeneric<T> where T : unmanaged
{
//...
}

Некероване обмеження передбачає обмеження структури, і воно не може бути поєднане ні з обмеженнями структури, ні з новими ().

Тип - це некерований тип, якщо це будь-який із наведених нижче типів:

  • sbyte, byte, short, ushort, int, uint, long, ulong, char, float, double, decimal або bool
  • Будь-який тип перерахунку
  • Будь-який тип вказівника
  • Будь-який визначений користувачем тип структури, який містить поля лише некерованих типів і, у C # 7.3 та новіших версіях, не є побудованим типом (тип, що включає принаймні один аргумент типу)

Для подальшого обмеження та усунення вказівних та визначених користувачем типів, які не реалізують IComparable, додайте IComparable (але enum все ще походить від IComparable, тому обмежте перерахунок, додавши IEquatable <T>, ви можете піти далі залежно від ваших обставин та додати додаткові інтерфейси. без керування дозволяє скоротити цей список):

    class SomeGeneric<T> where T : unmanaged, IComparable, IEquatable<T>
    {
    //...
    }

Приємно, але недостатньо ... Наприклад, DateTimeпідпадає під unmanaged, IComparable, IEquatable<T>обмеження ..
Адам Кальвет Боль

Я знаю, але ви можете піти далі залежно від ваших обставин і додати додаткові інтерфейси. некерований дозволяє скоротити цей список. Я щойно показав підхід, наближення з використанням некерованого. У більшості випадків цього достатньо
Влад Новаковський

4

Немає можливості обмежувати шаблони типами, але ви можете визначати різні дії залежно від типу. Як частина загального пакету чисел, мені потрібен був загальний клас, щоб додати два значення.

    class Something<TCell>
    {
        internal static TCell Sum(TCell first, TCell second)
        {
            if (typeof(TCell) == typeof(int))
                return (TCell)((object)(((int)((object)first)) + ((int)((object)second))));

            if (typeof(TCell) == typeof(double))
                return (TCell)((object)(((double)((object)first)) + ((double)((object)second))));

            return second;
        }
    }

Зауважте, що typeofs оцінюються під час компіляції, тому компілятор видаляє оператори if. Компілятор також видаляє помилкові касти. Так щось вирішиться в компіляторі до

        internal static int Sum(int first, int second)
        {
            return first + second;
        }

Дякуємо за надання емпіричного рішення!
zsf222

Хіба це не те саме, що створити однаковий метод для кожного типу?
Луїс

3

Я створив невелику функціональність бібліотеки для вирішення цих проблем:

Замість:

public T DifficultCalculation<T>(T a, T b)
{
    T result = a * b + a; // <== WILL NOT COMPILE!
    return result;
}
Console.WriteLine(DifficultCalculation(2, 3)); // Should result in 8.

Ви можете написати:

public T DifficultCalculation<T>(Number<T> a, Number<T> b)
{
    Number<T> result = a * b + a;
    return (T)result;
}
Console.WriteLine(DifficultCalculation(2, 3)); // Results in 8.

Ви можете знайти вихідний код тут: /codereview/26022/improvement-requested-for-generic-calculator-and-generic-number


2

Мені було цікаво так само, як самджудсон, чому тільки цілим числам? і якщо це так, ви можете створити клас помічників або щось подібне, щоб містити всі потрібні типи.

Якщо все, що ви хочете, - цілі числа, не використовуйте загальне, це не є загальним; а ще краще - відхилити будь-який інший тип, перевіривши його тип.


2

«Доброго» рішення для цього поки що немає. Однак ви можете значно звузити аргумент типу, щоб виключити безліч вад для вашого гіпотетичного обмеження "INumeric", як показав Haacked.

статичний bool IntegerFunction <T> (значення T), де T: ICпорівнянний, IFormattable, IConvertible, IComparable <T>, IEquatable <T>, struct {...


2

Якщо ви використовуєте .NET 4.0 і пізніші версії, ви можете просто використовувати динамику як аргумент методу і перевіряти під час виконання, що пройшла динаміка тип аргументу є числовим / цілим типом.

Якщо тип переданої динаміки не є числовим / цілим типом, викиньте виняток.

Приклад короткого коду, який реалізує цю ідею, виглядає приблизно так:

using System;
public class InvalidArgumentException : Exception
{
    public InvalidArgumentException(string message) : base(message) {}
}
public class InvalidArgumentTypeException : InvalidArgumentException
{
    public InvalidArgumentTypeException(string message) : base(message) {}
}
public class ArgumentTypeNotIntegerException : InvalidArgumentTypeException
{
    public ArgumentTypeNotIntegerException(string message) : base(message) {}
}
public static class Program
{
    private static bool IntegerFunction(dynamic n)
    {
        if (n.GetType() != typeof(Int16) &&
            n.GetType() != typeof(Int32) &&
            n.GetType() != typeof(Int64) &&
            n.GetType() != typeof(UInt16) &&
            n.GetType() != typeof(UInt32) &&
            n.GetType() != typeof(UInt64))
            throw new ArgumentTypeNotIntegerException("argument type is not integer type");
        //code that implements IntegerFunction goes here
    }
    private static void Main()
    {
         Console.WriteLine("{0}",IntegerFunction(0)); //Compiles, no run time error and first line of output buffer is either "True" or "False" depends on the code that implements "Program.IntegerFunction" static method.
         Console.WriteLine("{0}",IntegerFunction("string")); //Also compiles but it is run time error and exception of type "ArgumentTypeNotIntegerException" is thrown here.
         Console.WriteLine("This is the last Console.WriteLine output"); //Never reached and executed due the run time error and the exception thrown on the second line of Program.Main static method.
    }

Зрозуміло, що це рішення працює лише під час виконання, але ніколи в компільований час.

Якщо ви хочете рішення, яке завжди працює в час компіляції і ніколи не працює, тоді вам доведеться обернути динаміку загальнодоступною структурою / класом, перевантажений публікою конструкторами приймають лише аргументи потрібних типів і дають структурі / класу відповідне ім'я.

Має сенс, що загорнута динаміка - це завжди приватний член класу / struct, і це єдиний член структура / класу, а ім'я єдиного члена структури / класу - "значення".

Вам також доведеться визначати та впроваджувати громадськість методи та / або оператори, які працюють із потрібними типами для приватного динамічного члена класу / структури, якщо це необхідно.

Також має сенс, що структура / клас має спеціальний / унікальний конструктор, який приймає динаміку як аргумент, який ініціалізує, що це лише приватний динамічний член, який називається "value", але модифікатор цього конструктора є приватним звичайно, .

Після того, як клас / структура буде готова, визначте тип аргументу IntegerFunction таким класом / структурою, який був визначений.

Приклад довгий код, який реалізує ідею, виглядає приблизно так:

using System;
public struct Integer
{
    private dynamic value;
    private Integer(dynamic n) { this.value = n; }
    public Integer(Int16 n) { this.value = n; }
    public Integer(Int32 n) { this.value = n; }
    public Integer(Int64 n) { this.value = n; }
    public Integer(UInt16 n) { this.value = n; }
    public Integer(UInt32 n) { this.value = n; }
    public Integer(UInt64 n) { this.value = n; }
    public Integer(Integer n) { this.value = n.value; }
    public static implicit operator Int16(Integer n) { return n.value; }
    public static implicit operator Int32(Integer n) { return n.value; }
    public static implicit operator Int64(Integer n) { return n.value; }
    public static implicit operator UInt16(Integer n) { return n.value; }
    public static implicit operator UInt32(Integer n) { return n.value; }
    public static implicit operator UInt64(Integer n) { return n.value; }
    public static Integer operator +(Integer x, Int16 y) { return new Integer(x.value + y); }
    public static Integer operator +(Integer x, Int32 y) { return new Integer(x.value + y); }
    public static Integer operator +(Integer x, Int64 y) { return new Integer(x.value + y); }
    public static Integer operator +(Integer x, UInt16 y) { return new Integer(x.value + y); }
    public static Integer operator +(Integer x, UInt32 y) { return new Integer(x.value + y); }
    public static Integer operator +(Integer x, UInt64 y) { return new Integer(x.value + y); }
    public static Integer operator -(Integer x, Int16 y) { return new Integer(x.value - y); }
    public static Integer operator -(Integer x, Int32 y) { return new Integer(x.value - y); }
    public static Integer operator -(Integer x, Int64 y) { return new Integer(x.value - y); }
    public static Integer operator -(Integer x, UInt16 y) { return new Integer(x.value - y); }
    public static Integer operator -(Integer x, UInt32 y) { return new Integer(x.value - y); }
    public static Integer operator -(Integer x, UInt64 y) { return new Integer(x.value - y); }
    public static Integer operator *(Integer x, Int16 y) { return new Integer(x.value * y); }
    public static Integer operator *(Integer x, Int32 y) { return new Integer(x.value * y); }
    public static Integer operator *(Integer x, Int64 y) { return new Integer(x.value * y); }
    public static Integer operator *(Integer x, UInt16 y) { return new Integer(x.value * y); }
    public static Integer operator *(Integer x, UInt32 y) { return new Integer(x.value * y); }
    public static Integer operator *(Integer x, UInt64 y) { return new Integer(x.value * y); }
    public static Integer operator /(Integer x, Int16 y) { return new Integer(x.value / y); }
    public static Integer operator /(Integer x, Int32 y) { return new Integer(x.value / y); }
    public static Integer operator /(Integer x, Int64 y) { return new Integer(x.value / y); }
    public static Integer operator /(Integer x, UInt16 y) { return new Integer(x.value / y); }
    public static Integer operator /(Integer x, UInt32 y) { return new Integer(x.value / y); }
    public static Integer operator /(Integer x, UInt64 y) { return new Integer(x.value / y); }
    public static Integer operator %(Integer x, Int16 y) { return new Integer(x.value % y); }
    public static Integer operator %(Integer x, Int32 y) { return new Integer(x.value % y); }
    public static Integer operator %(Integer x, Int64 y) { return new Integer(x.value % y); }
    public static Integer operator %(Integer x, UInt16 y) { return new Integer(x.value % y); }
    public static Integer operator %(Integer x, UInt32 y) { return new Integer(x.value % y); }
    public static Integer operator %(Integer x, UInt64 y) { return new Integer(x.value % y); }
    public static Integer operator +(Integer x, Integer y) { return new Integer(x.value + y.value); }
    public static Integer operator -(Integer x, Integer y) { return new Integer(x.value - y.value); }
    public static Integer operator *(Integer x, Integer y) { return new Integer(x.value * y.value); }
    public static Integer operator /(Integer x, Integer y) { return new Integer(x.value / y.value); }
    public static Integer operator %(Integer x, Integer y) { return new Integer(x.value % y.value); }
    public static bool operator ==(Integer x, Int16 y) { return x.value == y; }
    public static bool operator !=(Integer x, Int16 y) { return x.value != y; }
    public static bool operator ==(Integer x, Int32 y) { return x.value == y; }
    public static bool operator !=(Integer x, Int32 y) { return x.value != y; }
    public static bool operator ==(Integer x, Int64 y) { return x.value == y; }
    public static bool operator !=(Integer x, Int64 y) { return x.value != y; }
    public static bool operator ==(Integer x, UInt16 y) { return x.value == y; }
    public static bool operator !=(Integer x, UInt16 y) { return x.value != y; }
    public static bool operator ==(Integer x, UInt32 y) { return x.value == y; }
    public static bool operator !=(Integer x, UInt32 y) { return x.value != y; }
    public static bool operator ==(Integer x, UInt64 y) { return x.value == y; }
    public static bool operator !=(Integer x, UInt64 y) { return x.value != y; }
    public static bool operator ==(Integer x, Integer y) { return x.value == y.value; }
    public static bool operator !=(Integer x, Integer y) { return x.value != y.value; }
    public override bool Equals(object obj) { return this == (Integer)obj; }
    public override int GetHashCode() { return this.value.GetHashCode(); }
    public override string ToString() { return this.value.ToString(); }
    public static bool operator >(Integer x, Int16 y) { return x.value > y; }
    public static bool operator <(Integer x, Int16 y) { return x.value < y; }
    public static bool operator >(Integer x, Int32 y) { return x.value > y; }
    public static bool operator <(Integer x, Int32 y) { return x.value < y; }
    public static bool operator >(Integer x, Int64 y) { return x.value > y; }
    public static bool operator <(Integer x, Int64 y) { return x.value < y; }
    public static bool operator >(Integer x, UInt16 y) { return x.value > y; }
    public static bool operator <(Integer x, UInt16 y) { return x.value < y; }
    public static bool operator >(Integer x, UInt32 y) { return x.value > y; }
    public static bool operator <(Integer x, UInt32 y) { return x.value < y; }
    public static bool operator >(Integer x, UInt64 y) { return x.value > y; }
    public static bool operator <(Integer x, UInt64 y) { return x.value < y; }
    public static bool operator >(Integer x, Integer y) { return x.value > y.value; }
    public static bool operator <(Integer x, Integer y) { return x.value < y.value; }
    public static bool operator >=(Integer x, Int16 y) { return x.value >= y; }
    public static bool operator <=(Integer x, Int16 y) { return x.value <= y; }
    public static bool operator >=(Integer x, Int32 y) { return x.value >= y; }
    public static bool operator <=(Integer x, Int32 y) { return x.value <= y; }
    public static bool operator >=(Integer x, Int64 y) { return x.value >= y; }
    public static bool operator <=(Integer x, Int64 y) { return x.value <= y; }
    public static bool operator >=(Integer x, UInt16 y) { return x.value >= y; }
    public static bool operator <=(Integer x, UInt16 y) { return x.value <= y; }
    public static bool operator >=(Integer x, UInt32 y) { return x.value >= y; }
    public static bool operator <=(Integer x, UInt32 y) { return x.value <= y; }
    public static bool operator >=(Integer x, UInt64 y) { return x.value >= y; }
    public static bool operator <=(Integer x, UInt64 y) { return x.value <= y; }
    public static bool operator >=(Integer x, Integer y) { return x.value >= y.value; }
    public static bool operator <=(Integer x, Integer y) { return x.value <= y.value; }
    public static Integer operator +(Int16 x, Integer y) { return new Integer(x + y.value); }
    public static Integer operator +(Int32 x, Integer y) { return new Integer(x + y.value); }
    public static Integer operator +(Int64 x, Integer y) { return new Integer(x + y.value); }
    public static Integer operator +(UInt16 x, Integer y) { return new Integer(x + y.value); }
    public static Integer operator +(UInt32 x, Integer y) { return new Integer(x + y.value); }
    public static Integer operator +(UInt64 x, Integer y) { return new Integer(x + y.value); }
    public static Integer operator -(Int16 x, Integer y) { return new Integer(x - y.value); }
    public static Integer operator -(Int32 x, Integer y) { return new Integer(x - y.value); }
    public static Integer operator -(Int64 x, Integer y) { return new Integer(x - y.value); }
    public static Integer operator -(UInt16 x, Integer y) { return new Integer(x - y.value); }
    public static Integer operator -(UInt32 x, Integer y) { return new Integer(x - y.value); }
    public static Integer operator -(UInt64 x, Integer y) { return new Integer(x - y.value); }
    public static Integer operator *(Int16 x, Integer y) { return new Integer(x * y.value); }
    public static Integer operator *(Int32 x, Integer y) { return new Integer(x * y.value); }
    public static Integer operator *(Int64 x, Integer y) { return new Integer(x * y.value); }
    public static Integer operator *(UInt16 x, Integer y) { return new Integer(x * y.value); }
    public static Integer operator *(UInt32 x, Integer y) { return new Integer(x * y.value); }
    public static Integer operator *(UInt64 x, Integer y) { return new Integer(x * y.value); }
    public static Integer operator /(Int16 x, Integer y) { return new Integer(x / y.value); }
    public static Integer operator /(Int32 x, Integer y) { return new Integer(x / y.value); }
    public static Integer operator /(Int64 x, Integer y) { return new Integer(x / y.value); }
    public static Integer operator /(UInt16 x, Integer y) { return new Integer(x / y.value); }
    public static Integer operator /(UInt32 x, Integer y) { return new Integer(x / y.value); }
    public static Integer operator /(UInt64 x, Integer y) { return new Integer(x / y.value); }
    public static Integer operator %(Int16 x, Integer y) { return new Integer(x % y.value); }
    public static Integer operator %(Int32 x, Integer y) { return new Integer(x % y.value); }
    public static Integer operator %(Int64 x, Integer y) { return new Integer(x % y.value); }
    public static Integer operator %(UInt16 x, Integer y) { return new Integer(x % y.value); }
    public static Integer operator %(UInt32 x, Integer y) { return new Integer(x % y.value); }
    public static Integer operator %(UInt64 x, Integer y) { return new Integer(x % y.value); }
    public static bool operator ==(Int16 x, Integer y) { return x == y.value; }
    public static bool operator !=(Int16 x, Integer y) { return x != y.value; }
    public static bool operator ==(Int32 x, Integer y) { return x == y.value; }
    public static bool operator !=(Int32 x, Integer y) { return x != y.value; }
    public static bool operator ==(Int64 x, Integer y) { return x == y.value; }
    public static bool operator !=(Int64 x, Integer y) { return x != y.value; }
    public static bool operator ==(UInt16 x, Integer y) { return x == y.value; }
    public static bool operator !=(UInt16 x, Integer y) { return x != y.value; }
    public static bool operator ==(UInt32 x, Integer y) { return x == y.value; }
    public static bool operator !=(UInt32 x, Integer y) { return x != y.value; }
    public static bool operator ==(UInt64 x, Integer y) { return x == y.value; }
    public static bool operator !=(UInt64 x, Integer y) { return x != y.value; }
    public static bool operator >(Int16 x, Integer y) { return x > y.value; }
    public static bool operator <(Int16 x, Integer y) { return x < y.value; }
    public static bool operator >(Int32 x, Integer y) { return x > y.value; }
    public static bool operator <(Int32 x, Integer y) { return x < y.value; }
    public static bool operator >(Int64 x, Integer y) { return x > y.value; }
    public static bool operator <(Int64 x, Integer y) { return x < y.value; }
    public static bool operator >(UInt16 x, Integer y) { return x > y.value; }
    public static bool operator <(UInt16 x, Integer y) { return x < y.value; }
    public static bool operator >(UInt32 x, Integer y) { return x > y.value; }
    public static bool operator <(UInt32 x, Integer y) { return x < y.value; }
    public static bool operator >(UInt64 x, Integer y) { return x > y.value; }
    public static bool operator <(UInt64 x, Integer y) { return x < y.value; }
    public static bool operator >=(Int16 x, Integer y) { return x >= y.value; }
    public static bool operator <=(Int16 x, Integer y) { return x <= y.value; }
    public static bool operator >=(Int32 x, Integer y) { return x >= y.value; }
    public static bool operator <=(Int32 x, Integer y) { return x <= y.value; }
    public static bool operator >=(Int64 x, Integer y) { return x >= y.value; }
    public static bool operator <=(Int64 x, Integer y) { return x <= y.value; }
    public static bool operator >=(UInt16 x, Integer y) { return x >= y.value; }
    public static bool operator <=(UInt16 x, Integer y) { return x <= y.value; }
    public static bool operator >=(UInt32 x, Integer y) { return x >= y.value; }
    public static bool operator <=(UInt32 x, Integer y) { return x <= y.value; }
    public static bool operator >=(UInt64 x, Integer y) { return x >= y.value; }
    public static bool operator <=(UInt64 x, Integer y) { return x <= y.value; }
}
public static class Program
{
    private static bool IntegerFunction(Integer n)
    {
        //code that implements IntegerFunction goes here
        //note that there is NO code that checks the type of n in rum time, because it is NOT needed anymore 
    }
    private static void Main()
    {
        Console.WriteLine("{0}",IntegerFunction(0)); //compile error: there is no overloaded METHOD for objects of type "int" and no implicit conversion from any object, including "int", to "Integer" is known.
        Console.WriteLine("{0}",IntegerFunction(new Integer(0))); //both compiles and no run time error
        Console.WriteLine("{0}",IntegerFunction("string")); //compile error: there is no overloaded METHOD for objects of type "string" and no implicit conversion from any object, including "string", to "Integer" is known.
        Console.WriteLine("{0}",IntegerFunction(new Integer("string"))); //compile error: there is no overloaded CONSTRUCTOR for objects of type "string"
    }
}

Зауважте, що для використання динамічного коду необхідно додати посилання на Microsoft.CSharp

Якщо версія .NET фреймворку знаходиться нижче / нижче / менше 4,0, а в цій версії динаміка не визначена, то вам доведеться замість цього використовувати об’єкт і робити кастинг на цілий тип, що є проблемою, тому рекомендую використовувати в принаймні .NET 4.0 або новіші, якщо ви можете, щоб ви могли використовувати динамічний замість об'єкта .


2

На жаль .NET не забезпечує спосіб зробити це на самому собі.

Для вирішення цього питання я створив бібліотеку OSS Genumerics, яка забезпечує більшість стандартних числових операцій для наступних вбудованих числових типів та їх нульових еквівалентів з можливістю додавати підтримку для інших числових типів.

sbyte, byte, short, ushort, int, uint, long, ulong, float, double, decimal, ІBigInteger

Продуктивність еквівалентна конкретному типовому рішенню, що дозволяє створювати ефективні загальні числові алгоритми.

Ось приклад використання коду.

public static T Sum(T[] items)
{
    T sum = Number.Zero<T>();
    foreach (T item in items)
    {
        sum = Number.Add(sum, item);
    }
    return sum;
}
public static T SumAlt(T[] items)
{
    // implicit conversion to Number<T>
    Number<T> sum = Number.Zero<T>();
    foreach (T item in items)
    {
        // operator support
        sum += item;
    }
    // implicit conversion to T
    return sum;
}

1

У чому сенс вправи?

Як вже зазначали люди, у вас може бути негенерована функція, яка займає найбільший елемент, і компілятор автоматично перетворить менші вставки для вас.

static bool IntegerFunction(Int64 value) { }

Якщо ваша функція перебуває у критичному для продуктивності шляху (дуже малоймовірно, IMO), ви можете забезпечити перевантаження для всіх необхідних функцій.

static bool IntegerFunction(Int64 value) { }
...
static bool IntegerFunction(Int16 value) { }

1
Я багато працюю з числовими методами. Іноді я хочу цілі числа, а іноді - плаваючу крапку. В обох є 64-бітні версії, оптимальні для швидкості обробки. Перетворення між ними - жахлива ідея, оскільки в кожному разі є втрати. Хоча я схильний до використання парних пар, іноді я вважаю, що краще використовувати цілі числа через те, як вони звикають в іншому місці. Але було б дуже приємно, коли я пишу алгоритм зробити це один раз і залишити рішення типу відповідно до вимог екземпляра.
VoteCoffee

1

Я б використав загальний, з яким ви могли б поводитися зовні ...

/// <summary>
/// Generic object copy of the same type
/// </summary>
/// <typeparam name="T">The type of object to copy</typeparam>
/// <param name="ObjectSource">The source object to copy</param>
public T CopyObject<T>(T ObjectSource)
{
    T NewObject = System.Activator.CreateInstance<T>();

    foreach (PropertyInfo p in ObjectSource.GetType().GetProperties())
        NewObject.GetType().GetProperty(p.Name).SetValue(NewObject, p.GetValue(ObjectSource, null), null);

    return NewObject;
}

1

Це обмеження вплинуло на мене, коли я намагався перевантажувати операторів для загальних типів; оскільки не існувало обмеження "Інумерія", і з-за ряду інших причин хороші люди в потоковому потоці із задоволенням забезпечують, операції неможливо визначити на загальних типах.

Я хотів чогось подібного

public struct Foo<T>
{
    public T Value{ get; private set; }

    public static Foo<T> operator +(Foo<T> LHS, Foo<T> RHS)
    {
        return new Foo<T> { Value = LHS.Value + RHS.Value; };
    }
}

Я працював над цією проблемою, використовуючи .net4 динамічний набір часу виконання.

public struct Foo<T>
{
    public T Value { get; private set; }

    public static Foo<T> operator +(Foo<T> LHS, Foo<T> RHS)
    {
        return new Foo<T> { Value = LHS.Value + (dynamic)RHS.Value };
    }
}

Дві речі щодо використання dynamicє

  1. Продуктивність. Усі типи значень отримують коробки.
  2. Помилки під час виконання. Ви "биєте" компілятор, але втрачаєте безпеку типу. Якщо для загального типу не визначено оператора, під час виконання буде викинуто виняток.

1

Числові примітивні типи .NET не мають загального інтерфейсу, який би дозволяв використовувати їх для обчислень. Можна було б визначити свої власні інтерфейси (наприклад ISignedWholeNumber), які виконували б такі операції, визначали структури, які містять єдиний Int16, Int32і т. Д. Та реалізують ці інтерфейси, а потім застосовують методи, які приймають загальні типи, обмежені ISignedWholeNumber, але мають перетворювати числові значення. для типів вашої структури, ймовірно, буде неприємність.

Альтернативний підхід полягає в визначення статичного класу Int64Converter<T>зі статичним властивістю bool Available {get;};і статичними делегатами Int64 GetInt64(T value), T FromInt64(Int64 value), bool TryStoreInt64(Int64 value, ref T dest). Конструктор класу може використовувати жорсткий код для завантаження делегатів для відомих типів і, можливо, використовувати Reflection для перевірки того, чи тип Tреалізує методи з власними іменами та підписами (на випадок, якщо це щось на зразок структури, яка містить Int64і представляє число, але має нестандартний ToString()метод). Цей підхід втратить переваги, пов'язані з перевіркою типу компіляції, але все-таки вдасться уникнути бокс-операцій, і кожен тип повинен був би бути перевірений лише один раз. Після цього операції, пов'язані з цим типом, будуть замінені делегатом.


@KenKin: IConvertible надає засоби, за допомогою яких будь-яке ціле число може бути додане до іншого цілого типу для отримання, наприклад, Int64результату, але не забезпечує засоби, за допомогою яких, наприклад, ціле число довільного типу можна збільшити, щоб отримати ще одне ціле число того ж типу .
supercat

1

У мене була подібна ситуація, коли мені потрібно було обробляти числові типи та рядки; здається трохи химерною сумішшю, але там ви йдете.

Знову ж таки, як багато людей, я переглянув обмеження і придумав купу інтерфейсів, які він повинен підтримувати. Однак, а) це не було 100% водонепроникним і б), хто-небудь новий, дивлячись на цей довгий перелік обмежень, був би негайно заплутаний.

Отже, мій підхід полягав у тому, щоб усю мою логіку перетворити на загальний метод без обмежень, а зробити цей загальний метод приватним. Потім я відкрив це загальнодоступними методами, один явно обробляв тип, з яким я хотів обробити - на мій погляд, код чистий і явний, наприклад

public static string DoSomething(this int input, ...) => DoSomethingHelper(input, ...);
public static string DoSomething(this decimal input, ...) => DoSomethingHelper(input, ...);
public static string DoSomething(this double input, ...) => DoSomethingHelper(input, ...);
public static string DoSomething(this string input, ...) => DoSomethingHelper(input, ...);

private static string DoSomethingHelper<T>(this T input, ....)
{
    // complex logic
}

0

Якщо все, що вам потрібно, це використовувати один числовий тип , ви можете розглянути можливість створення чогось схожого на псевдонім у C ++ using.

Тож замість того, щоб мати саме загальне

T ComputeSomething<T>(T value1, T value2) where T : INumeric { ... }

ви могли б мати

using MyNumType = System.Double;
T ComputeSomething<MyNumType>(MyNumType value1, MyNumType value2) { ... }

Це може допомогти вам легко перейти doubleдо intта інших людей, якщо це потрібно, але ви не зможете користуватися ComputeSomethingз тією ж програмою doubleта intв одній програмі.

Але чому б не замінити всі doubleна intпотім? Тому що ваш метод може захотіти використовувати doubleвхід doubleчи int. Псевдонім дозволяє точно знати, яка змінна використовує динамічний тип.


0

Тема стара, але для майбутніх читачів:

Ця функція тісно пов'язана з тим, Discriminated Unionsщо поки не реалізовано в C #. Я знайшов її випуск тут:

https://github.com/dotnet/csharplang/isissue/113

Це питання залишається відкритим, і функція була запланована на C# 10

Тож ще треба почекати ще трохи, але після випуску ви можете це зробити так:

static bool IntegerFunction<T>(T value) where T : Int16 | Int32 | Int64 | ...

-11

Я думаю, ви нерозумієте дженерики. Якщо операція, яку ви намагаєтеся виконати, корисна лише для конкретних типів даних, тоді ви не робите щось "загальне".

Крім того, оскільки ви хочете лише дозволити функції працювати над типами даних int, то вам не знадобиться окрема функція для кожного конкретного розміру. Просто взяття параметра в найбільшому конкретному типі дозволить програмі автоматично оновити на неї менші типи даних. (тобто передача Int16 автоматично перетворюється на Int64 під час виклику).

Якщо ви виконуєте різні операції на основі фактичного розміру int, який передається у функцію, то я думаю, вам варто серйозно переглянути, навіть намагаючись робити те, що ви робите. Якщо вам доведеться обдурити мову, вам слід подумати трохи більше про те, що ви намагаєтеся досягти, а не як робити те, що хочете.

Якщо все інше не вдалося, може бути використаний параметр типу Object, і тоді вам доведеться перевірити тип параметра і вжити відповідних дій або викинути виняток.


10
Розглянемо гістограму класу <T>. Має сенс дозволити йому прийняти загальний параметр, тому компілятор може оптимізувати його для байтів, ints, подвійних, десяткових, BigInt, ... але в той же час вам потрібно запобігти створенню, скажімо, гістограми <Hashset >, тому що - якщо говорити з Tron - це не обчислити. (буквально :))
сонця

15
Ти той, хто неправильно розуміє дженерики. Метапрограмування не просто працює на значеннях, які можуть бути будь-якого можливого типу , це для роботи на типах, що відповідають різним обмеженням .
Джим Балтер
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.