Show / Hide Table of Contents

MSDN.WhiteKnight - Stack Overflow answers

Ответ на "Как написать метод/класс, который бы одинаково работал со всеми числовыми типами?"

Answer 901079

Link

Обновлено: В .NET 7 появилась встроенная поддержка Generic math, так что приведенные в данной теме ухищрения становятся не нужны. Чтобы создать метод, принимающий любое число, нужно ограничить обобщенный тип интерфейсом INumber:

static T Add<T>(T x, T y) where T : INumber<T>
{
    return x + y;
}

Ответ для предыдущих версий:

Все числовые типы объединяет то, что они являются структурами и реализуют интерфейс IComparable. С этим ограничением уже можно отсечь много неподходящих типов на этапе компиляции. Не нужно использовать статические конструкторы для "валидации", они предназначены для инициализации глобального состояния, и класс, единственная задача которого - арифметические операции, вообще не должен их иметь. Проверяйте перед вычислением (или компиляцией выражения), это намного более логично.

Что касается алгоритма, есть еще один способ, который лежит на поверхности: это простой обобщенный метод с несколькими ветками в условном операторе. Может показаться, что веток будет слишком много, но на самом деле, операции сложения для многих типов по сути одинаковы и отличаются только типом, к которому приводится конечный результат. Например, операцию сложения на целом типе можно представить как операцию сложения на Decimal с последующим "сужающим" приведением к целому типу (Decimal позволяет представить все значения любых целых типов и еще оставляет некоторый запас для обработки переполнений). Аналогично, сложение на типе float можно представить как сложение на типе double с последующим преобразованием результата.

Весь набор числовых типов можно разделить на три группы:

  1. Беззнаковые целые. Для них формула преобразования из Decimal в конкретный тип будет выглядеть так:

y = x % 2 n

где n - размер типа в битах.

(Остаток от деления тут появляется, так как по умолчанию у нас unchecked-контекст, и переполнения не генерируют ошибку, а просто обрезаются по границе типа.)

  1. Знаковые целые. Для них минимальное значение равно - 0.5 * 2 n, а максимальное 0.5 * 2 n - 1. Пользуясь этим, можно вывести формулу перевода:

y = (x + 2 n * 1.5) % 2 n - 0.5 * 2 n

На самом деле, формула может выглядеть по разному, но для отлова переполнений подходит именно такой вид.

  1. С плавающей точкой. Ну, тут все просто, формула не нужна, так как преобразование из double в float это просто обрезка "знаков после запятой".

Реализовать это можно так:

using System;
using System.Text;

namespace ConsoleApp1
{
    public class Calculator<T> where T : struct,IComparable
    {
        static bool IsSignedInteger(Type t)
        {
            return (t == typeof(sbyte) || t == typeof(short) || t == typeof(int) || t == typeof(long));
        }

        static bool IsUnsignedInteger(Type t)
        {
            return (t == typeof(byte) || t == typeof(ushort) || t == typeof(uint) || t == typeof(ulong));
        }

        static bool IsReal(Type t)
        {
            return (t == typeof(float) || t == typeof(double));
        }

        //преобразует значение из Decimal в целевой целочисленный тип
        public static T FromDecimal(decimal val)
        {
            //вычисляем размер типа
            int size = System.Runtime.InteropServices.Marshal.SizeOf(typeof(T));

            //вычисляем количество элементов в целевом множестве
            decimal capacity = (size < 8) ? (1L << (size * 8)) : ((decimal)UInt64.MaxValue + 1);

            //отображаем элемент на целевое множество
            decimal res;

            if (IsUnsignedInteger(typeof(T)))
            {
                res = (val) % (capacity);
                return (T)Convert.ChangeType(res, typeof(T));
            }
            else if (IsSignedInteger(typeof(T)))
            {    
                res = (val + capacity * 1.5M) % (capacity) - capacity * 0.5M;    
                return (T)Convert.ChangeType(res, typeof(T));
            }
            else throw new NotSupportedException(typeof(T).ToString() + " is not integer type");
        }

        //непосредственно сложение
        public static T Add(T A, T B)
        {
            if (IsSignedInteger(typeof(T)) || IsUnsignedInteger(typeof(T)))
            {
                return FromDecimal(Convert.ToDecimal(A) + Convert.ToDecimal(B));
            }
            else if (IsReal(typeof(T)))
            {
                return (T)Convert.ChangeType(Convert.ToDouble(A) + Convert.ToDouble(B), typeof(T));
            }
            else throw new NotSupportedException(typeof(T).ToString() + " is not supported, because it is not numeric type");
        }
    }

    class Program
    {
        static void Main(string[] args)
        {    
            unchecked
            {
                //тест сложения целых чисел
                Console.WriteLine("{0} {1}", Calculator<int>.Add(1000, 222), (1000 + 222));
                Console.WriteLine("{0} {1}", Calculator<byte>.Add(200, 200), (byte)(200 + 200));
                Console.WriteLine("{0} {1}", Calculator<sbyte>.Add(100, 100), (sbyte)(100 + 100));                        
                Console.WriteLine("{0} {1}", Calculator<long>.Add(long.MinValue, -1), (long)(long.MinValue - 1));

                //тест сложения с плавающей точкой    
                Console.WriteLine("{0} {1}", Calculator<float>.Add((float)Math.PI, 2.2f), (float)Math.PI + 2.2f);
                Console.WriteLine("{0} {1}", Calculator<double>.Add(Math.PI, 2.2), Math.PI + 2.2);
                
                //этот код выдаст исключение...                
                //Console.WriteLine("{0}", Calculator<DateTime>.Add(DateTime.Now, new DateTime(2000, 1, 1)));
                //Console.WriteLine("{0}", Calculator<bool>.Add(true, true));

                //а этот - не скомпилируется
                //Console.WriteLine("{0}", Calculator<string>.Add("Саша", "Маша"));
                
            } 
            Console.ReadKey();
        }              
    }
}

Если наплевать на переполнения, то код можно значительно упростить.


Content is retrieved from StackExchange API.

Auto-generated by ruso-archive tools.

Back to top Stack Overflow answers (published from sources in GitHub repository). Copyright (c) 2020, MSDN.WhiteKnight. Content licensed under BSD 3-Clause License.
Generated by DocFX