RError.com

RError.com Logo RError.com Logo

RError.com Navigation

  • 主页

Mobile menu

Close
  • 主页
  • 系统&网络
    • 热门问题
    • 最新问题
    • 标签
  • Ubuntu
    • 热门问题
    • 最新问题
    • 标签
  • 帮助
主页 / 问题 / 899928
Accepted
Kir_Antipov
Kir_Antipov
Asked:2020-10-30 21:47:34 +0000 UTC2020-10-30 21:47:34 +0000 UTC 2020-10-30 21:47:34 +0000 UTC

如何编写一个对所有数字类型都适用的方法/类?

  • 772

假设我想描述generic一个充当计算器的 -class,以便它对所有数字类型都以相同的方式工作。那些。:

  • 字节
  • 字节
  • 短的
  • 超短
  • 整数
  • 单位
  • 长
  • 乌龙
  • 漂浮
  • 双倍的

任务的复杂性在于,尽管所有这些类型都通过为其算术运算实例的定义而联合起来,但是,它们不继承任何类型的公共接口IArithmetical, INumber。而且其中的where 约束C#也不允许我们描述如下内容:

public static void Add<T>(T A, T B) where T : +, -, *, /, % ...

所以编译器不能确定利用该方法的所有可能类型都定义了正确的操作。

这会产生如下代码:

public class Calculator<T>
{
    public T Add(T A, T B) => A + B;
    public T Sub(T A, T B) => A - B;
    public T Mul(T A, T B) => A * B;
    public T Div(T A, T B) => A / B;
    public T Mod(T A, T B) => A % B;
}

唉,根本无法编译:由于上述原因,会抛出错误CS0019


那么在这种情况下该怎么办呢?一般来说,是否有可能C#描述generic一个可以与数字一起使用并且只能与它们一起使用的类/方法?

c#
  • 3 3 个回答
  • 10 Views

3 个回答

  • Voted
  1. Best Answer
    Kir_Antipov
    2020-10-30T21:47:34Z2020-10-30T21:47:34Z

    我开始这个话题是为了考虑所有已知的(对我来说)解决问题的方法,并对这个相当常见的问题给出最详细的答案。

    如果你突然知道我出于某种原因没有在这篇文章中描述的方法 - 请在评论中写下它!

    所以我们走吧!


    #0.0:代码重复且没有泛型

    毫无疑问,最平庸的解决方案,所有绝望的求助,只是为每种类型复制代码:

    public sbyte Add(sbyte A, sbyte B) => (sbyte)(A + B);
    public byte Add(byte A, byte B) => (byte)(A + B);
    public short Add(short A, short B) => (short)(A + B);
    public ushort Add(ushort A, ushort B) => (ushort)(A + B);
    public int Add(int A, int B) => A + B;
    public uint Add(uint A, uint B) => A + B;
    public long Add(long A, long B) => A + B;
    public ulong Add(ulong A, ulong B) => A + B;
    public float Add(float A, float B) => A + B;
    public double Add(double A, double B) => A + B;
    

    优点:

    • 至少它有效)
    • 操作仅适用于指定类型
    • 类型的操作是在编译时定义的(无需在运行时牺牲时间和其他资源来创建这些方法)

    缺点:

    • 显然,这是一堆重复的代码垃圾,破坏了项目的所有优雅。
    • 如果方法突然Add稍微改变它的逻辑(不管听起来多么奇怪),那么N个方法中的每一个都必须重写!

    #0.1:代码生成

    为了以某种方式简化您的生活并且不编写大量相同类型的方法,您可以使用内置的Visual Studio T4 生成器进行代码生成:

    让我们Calc.tt根据模板Text Template(Текстовый шаблон)将文件添加到项目中。让我们在其中编写以下代码:

    <#@ output extension=".cs" #>
    <#@ assembly name="System.Core" #>
    <#@ import namespace="System.Linq" #>
    <#@ import namespace="System.Text" #>
    <#@ import namespace="System.Collections.Generic" #>
    <#@ template debug="false" hostspecific="false" language="C#" #>
    
    namespace Calc
    {
        public class Calculator
        {
        <# 
            // Типы, используемые в методах
            string[] usingTypes = new[] { "sbyte", "byte", "short", "ushort", "int", "uint", "long", "ulong", "float", "double" };
    
            // Экземпляры некоторых типов перед операцией кастятся к int, так что результат нужно привести обратно
            HashSet<string> needCast= new HashSet<string> { "sbyte", "byte", "short", "ushort" };
    
            foreach(string T in usingTypes)
            {
        #>
            public <#=T#> Add(<#=T#> A, <#=T#> B) => <#=(needCast.Contains(T) ? $"({T})(A + B)" : "A + B")#>;
        <#
            }
        #>
        }
    }
    

    输出 ( Calc.cs) 将如下所示:

    namespace Calc
    {
        public class Calculator
        {
            public sbyte Add(sbyte A, sbyte B) => (sbyte)(A + B);
            public byte Add(byte A, byte B) => (byte)(A + B);
            public short Add(short A, short B) => (short)(A + B);
            public ushort Add(ushort A, ushort B) => (ushort)(A + B);
            public int Add(int A, int B) => A + B;
            public uint Add(uint A, uint B) => A + B;
            public long Add(long A, long B) => A + B;
            public ulong Add(ulong A, ulong B) => A + B;
            public float Add(float A, float B) => A + B;
            public double Add(double A, double B) => A + B;
        }
    }
    

    优点:

    • 见第0.0段)
    • 我们摆脱了修改方法逻辑的问题:现在所有的逻辑都集中在一个地方,所以只编辑一次算法就足够了,代码生成器会为你做所有的脏活)

    缺点:

    • 委婉地说,对文本模板的语法高亮的支持是Visual Studio蹩脚的。所以你可以忘记舒适的编码)
      • 这仍然是不可取的,并且只是轻微覆盖了代码重复。

    这种方法最初在Alexander Petrov 的回答中有所描述


    #1.0:动态

    由于我们正在使用动态类型解决一个语言不知道的问题,下一个明显的解决方案是使用dynamic:

    类型dynamic包括用于绕过编译时类型检查的操作。此类操作在运行时解决。

    其实很适合我们!

    让我们像这样重写方法Add:

    // Достаточно привести лишь один аргумент к dynamic,
    // дабы обозначить динамический контекст
    public T Add(T A, T B) => (T)((dynamic)A + B);
    

    让我们看看发生了什么:

    Calculator<int> calcInt = new Calculator<int>();
    int resultInt = calcInt.Add(19, 23); // 42
    
    Calculator<sbyte> calcSbyte = new Calculator<sbyte>();
    sbyte resultSbyte = calcSbyte.Add(19, 23); // 42
    

    一切似乎都很好!
    对,不过我得在这勺蜂蜜里加一桶焦油:

    除了解析动态上下文比解析静态上下文消耗更多时间之外,我们没有预见到以下事情:

    谁在阻止我们这样写?

    Calculator<DateTime> calcDate = new Calculator<DateTime>();
    DateTime resultDate = calcDate.Add(DateTime.Now, DateTime.Now);
    

    同样,没有人,所以代码会安静地编译,会成功创建一个类的实例,但是方法Add会失败并出现以下错误:

    Microsoft.CSharp.RuntimeBinder.RuntimeBinderException:无法将运算符+应用于类型System.DateTime和操作数System.DateTime

    正如问题本身所提到的,我们不能将 -parameters限制generic为一组特定的类型。所以你必须手动并在runtime:

    public class Calculator<T>
    {
        // Добавим классу статический инициализатор, который будет отвечать 
        // за проверку валидности типа
    
        // (Статический - дабы не проводить проверку несколько раз для одинаковых типов)
        static Calculator()
        {
            // Если тип не является одним из доступных, то сразу же выкинем ошибку,
            // дав тем самым конечному пользователю понять, что его 
            // действия неправомерны
            if (!new[] { typeof(sbyte), typeof(byte),
                        typeof(short), typeof(ushort),
                        typeof(int), typeof(uint),
                        typeof(long), typeof(ulong),
                        typeof(float), typeof(double)}.Contains(typeof(T)))
                throw new NotSupportedException($"Type `{typeof(T).FullName}` isn't supported!");
        }
    
        // Достаточно привести лишь один аргумент к dynamic,
        // дабы обозначить динамический контекст
        public T Add(T A, T B) => (T)((dynamic)A + B);           
    }
    

    优点:

    • 我们摆脱了代码重复
    • 我们终于添加到代码generic中,以便不同的预定义方法适用于不同的类型

    缺点:

    • 动态上下文处理的“精彩”速度
    • 需要在运行时检查所用类型的有效性

    #2.0:根据类型改变函数上下文

    该方法背后的思想如下:

    我们不能像这样在运行时重新分配函数:

    public void A() => ...;
    ...
    A = () => Console.WriteLine("Hello, world!");
    

    但是,我们可以重新分配变量(包括委托类型)!

    我们可以创建一个委托类型的内部字段,根据情况重新分配它,而公共方法是不可变的,只会利用它:

    public class Calculator<T>
    {
        static Calculator()
        {
            // Инициализируем _add, исходя из типа generic-параметра
            if (typeof(T) == typeof(sbyte))
                _add = castFrom<sbyte>((x, y) => (sbyte)(x + y));
            else if (typeof(T) == typeof(byte))
                _add = castFrom<byte>((x, y) => (byte)(x + y));
            else if (typeof(T) == typeof(short))
                _add = castFrom<short>((x, y) => (short)(x + y));
            else if (typeof(T) == typeof(ushort))
                _add = castFrom<ushort>((x, y) => (ushort)(x + y));
            else if (typeof(T) == typeof(int))
                _add = castFrom<int>((x, y) => x + y);
            else if (typeof(T) == typeof(uint))
                _add = castFrom<uint>((x, y) => x + y);
            else if (typeof(T) == typeof(long))
                _add = castFrom<long>((x, y) => x + y);
            else if (typeof(T) == typeof(ulong))
                _add = castFrom<ulong>((x, y) => x + y);
            else if (typeof(T) == typeof(int))
                _add = castFrom<float>((x, y) => x + y);
            else if (typeof(T) == typeof(double))
                _add = castFrom<double>((x, y) => x + y);
            else
                // Если тип не является ни одним из доступных, то выкинем ошибку 
                throw new NotSupportedException($"Type `{typeof(T).FullName}` isn't supported!");
    
            Func<T, T, T> castFrom<U>(Func<U, U, U> f) => (Func<T, T, T>)(object)f;
        }
    
        // Инструкция внутри _add будет проинициализированна
        // в зависимости от типа generic-параметра
        private static readonly Func<T, T, T> _add;
    
        // А вот инструкция в самой функции Add всегда одна - вызвать _add)
        public T Add(T A, T B) => _add(A, B);           
    }
    

    这种方法应该放在“重复代码”的板块中,但对很多人来说仍然没有那么明显dynamic,可以展开主题,在下一个板块中展示)

    优点:

    • 这将比以前使用动态上下文的方法快得多。

    缺点:

    • 我们再次滥用代码重复(再次,您可以将此方法与代码生成结合使用)
    • 需要在运行时检查所用类型的有效性

    这种方法最初是在VladD 的回答中描述的


    #2.1:表达式:

    使用Expression类,我们可以通过节点组装我们需要的表达式树,并编译成所需签名的委托,使用上一种方法的基本思想:

    public class Calculator<T>
    {
        static Calculator()
        {
            // Эту проверку Вы уже наблюдали)
            if (!new[] { typeof(sbyte), typeof(byte),
                typeof(short), typeof(ushort),
                typeof(int), typeof(uint),
                typeof(long), typeof(ulong),
                typeof(float), typeof(double)}.Contains(typeof(T)))
                throw new NotSupportedException($"Type `{typeof(T).FullName}` isn't supported!");
    
            // Укажем параметры, испоьзуемые в функции
            ParameterExpression a = Expression.Parameter(typeof(T));
            ParameterExpression b = Expression.Parameter(typeof(T));
    
            // Создадим узел сложения заданных параметров
            BinaryExpression addition = Expression.Add(a, b);
    
            // Скомпилируем полученное дерево
            _add = Expression.Lambda<Func<T, T, T>>(addition, a, b).Compile();
        }
    
        // Инструкция внутри _add будет проинициализированна
        // в зависимости от типа generic-параметра
        private static readonly Func<T, T, T> _add;
    
        // А вот инструкция в самой функции Add всегда одна - вызвать _add)
        public T Add(T A, T B) => _add(A, B);
    }
    

    优点:

    • 我们再次摆脱了代码重复
    • 就速度而言,这接近于非generic实现)

    缺点:

    • 您将不得不花费一些资源来runtime创建方法
    • 需要在运行时检查所用类型的有效性

    这种方法最初在Pavel Mayorov 的回答中有所描述


    #3.0:矢量化

    我们Microsoft有以下漂亮的包System.Numerics.Vectors,其描述如下:

    提供适用于高性能处理和图形应用程序的硬件加速数字类型。

    在这个包中,我们对Vector<T>类型感兴趣,它能够对输入数据进行矢量化处理,然后我们可以将所需的算术运算应用于接收到的矢量!

    让我们看一个例子:

    public class Calculator<T> where T : struct
    {
        static Calculator()
        {
            // Эту проверку Вы уже наблюдали)
            if (!new[] { typeof(sbyte), typeof(byte),
                    typeof(short), typeof(ushort),
                    typeof(int), typeof(uint),
                    typeof(long), typeof(ulong),
                    typeof(float), typeof(double)}.Contains(typeof(T)))
                throw new NotSupportedException($"Type `{typeof(T).FullName}` isn't supported!");
        }
    
        // Создадим векторы на основе заданных значений,
        // после чего сложим их и вернем 0-вое измерение
        // результирующего вектора
        public T Add(T A, T B) => (new Vector<T>(A) + new Vector<T>(B))[0];
    }
    

    优点:

    • 没有代码重复
    • 高性能(在某些测试中,向量化显示自己甚至比原生代码更好!哪些操作会更快 - 请参阅此表)

    缺点:

    • 需要安装额外的nuget-package
    • 需要在运行时检查所用类型的有效性

    这种方法最初是在VladD 的回答中描述的


    #4.0: IL позволит Вам то, чего не позволит C#!

    Как известно, код любого .NET-языка транслируется в IL-код. Этот факт мы и будем использовать)

    Напишем такой вот код:

    int a = 2;
    int b = 3;
    int c = a + b;
    

    Просмотрев IL-код, созданный для данной цепочки выражений, мы увидим нечто такое:

    ldc.i4.2    
    stloc a
    ldc.i4.3    
    stloc b
    ldloc a
    ldloc b
    add         
    stloc c
    

    (Код примерный, таким он, конечно, не будет. Приведен он в таком виде для ясности происходящего)

    Что же отвечает за сложение двух чисел типа int?
    Стандартная инструкция add)

    Перепишем код:

    double a = 2;
    double b = 3;
    double c = a + b;
    

    Теперь IL будет таковым:

    ldc.r8 2
    stloc a
    ldc.r8 3
    stloc b
    ldloc a
    ldloc b
    add         
    stloc c
    

    Что изменилось? Только инструкция loadconstant, инструкция же сложения так и осталось на своем законном месте)

    Я веду к тому, что на уровне IL одна и та же инструкция add спокойненько обрабатывает сложение экземпляров типов sbyte, byte, short, ushort, int, uint, long, ulong, float, double)
    А ведь это именно то, что нам нужно!

    (К слову, это верно и для инструкций sub, mul, div, rem. Подробный лист инструкций IL с описанием найдете здесь)

    Добавим к проекту файл Calc.il, используя расширение ILSupport, после чего запишем туда следующий код:

    .class Calc.Calculator`1<T>
    {
        .method public !T Add(!T, !T) cil managed
        {
            .maxstack 2
            ldarg.0     // Кладем на стек нулевой аргумент
            ldarg.1     // Кладем на стек первый аргумент
            add         // Складываем их
            ret         // Возвращаем результат
        }
    }
    

    На C# же проделаем следующие манипуляции с классом:

    public class Calculator<T>
    {
        static Calculator()
        {
            // Эту проверку Вы уже наблюдали)
            if (!new[] { typeof(sbyte), typeof(byte),
                    typeof(short), typeof(ushort),
                    typeof(int), typeof(uint),
                    typeof(long), typeof(ulong),
                    typeof(float), typeof(double)}.Contains(typeof(T)))
                throw new NotSupportedException($"Type `{typeof(T).FullName}` isn't supported!");
        }
    
        // Сообщаем, что метод реализован где-то в другом месте
        [MethodImpl(MethodImplOptions.ForwardRef)]
        public extern T Add(T A, T B);
    }
    

    Вот и готово! Скомпилировав проект, мы получим класс, который способен работать с любым стандартным числовым типом)

    Плюсы:

    • Никакого дублирования кода
    • Не нужно тратить времени и прочих ресурсов на создание метода в runtime
    • Высокая производительность, совпадающая с таковой нативного кода (ибо это он по сути и есть))

    Минусы:

    • Для человека, который не знаком с IL, решение может показаться сложным
    • Необходимо настроить проект на "сожительство" C# и IL
    • Необходимость проверять допустимость используемого типа уже во время исполнения

    Данный подход был первоначально описан в ответе от Kir_Antipov


    Надеюсь, один из предложенных в данном ответе методов помог решить Вам указанную задачу)

    А пока у меня есть 2 большие просьбы:

    1. Не забывайте благодарить авторов оригинальных ответов (помимо своего решения я собрал в данном ответе и идеи других участников сообщества, приведя на них ссылки)

    2. Если у Вас есть еще идеи по решению данной задачи/по исправлению данного ответа - пишите комментарии! Буду безумно рад выслушать Ваше мнение)

    • 16
  2. B. Vandyshev
    2020-10-31T07:41:56Z2020-10-31T07:41:56Z

    Мне нравятся подходы 0.0 и 0.1 из предыдущего ответа, но дублирование кода я бы сделал по-другому принципу, я бы вынес интерфейс ICalculator<T>:

    public interface ICalculator<T>
    {
        T Add(T a, T b);
        T Sub(T a, T b);
        T Mul(T a, T b);
        T Div(T a, T b);
        T Mod(T a, T b);
    }
    
    public class IntCalculator : ICalculator<int>
    {        
        public int Add(int a, int b) => a + b;
        public int Sub(int a, int b) => a - b;
        public int Mul(int a, int b) => a * b;
        public int Div(int a, int b) => a / b;
        public int Mod(int a, int b) => a % b;
    }
    

    Потребуется реализовать ICalculator<T> для каждого типа с которым вы хотите работать, они не обязательно должны быть числами. Такой подход будет гораздо лучше если вы используете Dependency Injection, вы сможете передавать ICalculator<T> в класс где он требуется. Ну и напоследок - реализация может быть либо такой, либо можно создать универсальную с dynamic или IL.

    • 4
  3. MSDN.WhiteKnight
    2020-11-02T03:23:47Z2020-11-02T03:23:47Z

    Все числовые типы объединяет то, что они являются структурами и реализуют интерфейс 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();
            }              
        }
    }
    

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

    • 2

相关问题

Sidebar

Stats

  • 问题 10021
  • Answers 30001
  • 最佳答案 8000
  • 用户 6900
  • 常问
  • 回答
  • Marko Smith

    是否可以在 C++ 中继承类 <---> 结构?

    • 2 个回答
  • Marko Smith

    这种神经网络架构适合文本分类吗?

    • 1 个回答
  • Marko Smith

    为什么分配的工作方式不同?

    • 3 个回答
  • Marko Smith

    控制台中的光标坐标

    • 1 个回答
  • Marko Smith

    如何在 C++ 中删除类的实例?

    • 4 个回答
  • Marko Smith

    点是否属于线段的问题

    • 2 个回答
  • Marko Smith

    json结构错误

    • 1 个回答
  • Marko Smith

    ServiceWorker 中的“获取”事件

    • 1 个回答
  • Marko Smith

    c ++控制台应用程序exe文件[重复]

    • 1 个回答
  • Marko Smith

    按多列从sql表中选择

    • 1 个回答
  • Martin Hope
    Alexandr_TT 圣诞树动画 2020-12-23 00:38:08 +0000 UTC
  • Martin Hope
    Suvitruf - Andrei Apanasik 什么是空? 2020-08-21 01:48:09 +0000 UTC
  • Martin Hope
    Air 究竟是什么标识了网站访问者? 2020-11-03 15:49:20 +0000 UTC
  • Martin Hope
    Qwertiy 号码显示 9223372036854775807 2020-07-11 18:16:49 +0000 UTC
  • Martin Hope
    user216109 如何为黑客设下陷阱,或充分击退攻击? 2020-05-10 02:22:52 +0000 UTC
  • Martin Hope
    Qwertiy 并变成3个无穷大 2020-11-06 07:15:57 +0000 UTC
  • Martin Hope
    koks_rs 什么是样板代码? 2020-10-27 15:43:19 +0000 UTC
  • Martin Hope
    Sirop4ik 向 git 提交发布的正确方法是什么? 2020-10-05 00:02:00 +0000 UTC
  • Martin Hope
    faoxis 为什么在这么多示例中函数都称为 foo? 2020-08-15 04:42:49 +0000 UTC
  • Martin Hope
    Pavel Mayorov 如何从事件或回调函数中返回值?或者至少等他们完成。 2020-08-11 16:49:28 +0000 UTC

热门标签

javascript python java php c# c++ html android jquery mysql

Explore

  • 主页
  • 问题
    • 热门问题
    • 最新问题
  • 标签
  • 帮助

Footer

RError.com

关于我们

  • 关于我们
  • 联系我们

Legal Stuff

  • Privacy Policy

帮助

© 2023 RError.com All Rights Reserve   沪ICP备12040472号-5