Значение по умолчанию для структуры Rational Number

Я работаю над простой математической библиотекой для образовательных целей, и я реализовал struct, который представляет Rational Number. Очень простой код, показывающий основные поля структуры:

public struct RationalNumber
{
    private readonly long numerator;
    private readonly long denominator;
    private bool isDefinitelyCoprime;
    private static RationalNumber zero = 0;

    public RationalNumber(long numerator, long denominator)
    {
        this.numerator = numerator;
        this.denominator = denominator;
        this.isDefinitelyCoprime = false;
    }

    ...
}

В настоящее время я реализую RationalMatrix, который, как вы уже догадались, будет состоять из RationalNumber типизированных элементов.

Полезной матрицей, для которой я создаю статический строитель, является матрица Identity. Код выглядит следующим образом:

public static RationalMatrix GetIdentityMatrix(int dimension)
{
    RationalNumber[,] values = new RationalNumber[dimension, dimension];

    for (int i = 0; i < dimension; i++)
       values[i, i] = 1;

    return new RationalMatrix(values);
}

Проблема заключается в том, что это не будет работать, потому что значение по умолчанию моего RationalNumber не 0/1, а 0/0, которое является особым видом значения (Неопределенная форма).

Очевидно, одно решение простое, и просто нужно изменить метод:

public static RationalMatrix GetIdentityMatrix(int dimension)
{
    RationalNumber[,] values = new RationalNumber[dimension, dimension];

    for (int i = 0; i < dimension; i++)
       for (int j = i+1 ; j < dimension; j++)
       {
           values[i, i] = 1;
           values[i, j] = RationalNumber.Zero;
           values[j, i] = RationalNumber.Zero;
       }

       return new RationalMatrix(values);
}

Но это как-то кажется пустой тратой усилий, поскольку я в основном инициализирую значения всего массива два раза. Я думаю, что было бы более элегантно каким-то образом сделать значение по умолчанию RationalNumber равным 0/1. Это было бы легко сделать, если RationalNumber был class, но я не могу придумать способ сделать это, когда он struct. Я пропустил что-то очевидное или нет способа избежать использования 0/0 в качестве значения по умолчанию?

Я хотел бы отметить, что меня вообще не интересует производительность кода (если это будет моим узким местом, то я уже далеко не смогу достичь своих целей). Мне просто интересно узнать, есть ли какая-нибудь конструкция (неизвестная мне), которая позволяет вам вводить произвольные значения по умолчанию в struct.

EDIT: Typos

РЕДАКТИРОВАТЬ 2: Расширить круг вопросов

ОК, похоже, что невозможно ввести произвольные значения по умолчанию в struct из ввода, который я получаю, и из моих собственных выводов, основанных на моих ограниченных знаниях на С#.

Может кто-нибудь дать мне понять, почему структуры должны вести себя таким образом? Это по какой-то причине или было реализовано таким образом, потому что никто не думал указать параметр для определения значений по умолчанию?

Ответ 1

Если вам не нужно различать неопределенные значения 0/0 и другие значения 0/N, вы можете обрабатывать все свои 0/N как ноль. То есть все нули равны, что имеет смысл (0/2 равно 0/1), а также все деления на ноль равны, поэтому 1/0 == 2/0.

public struct RationalNumber : IEquatable<RationalNumber>
{
    private readonly long numerator;
    private readonly long denominator;

    public RationalNumber(long numerator, long denominator)
    {
        this.numerator = numerator;
        this.denominator = denominator;
    }

    public bool IsZero 
    { 
       get { return numerator == 0; }
    }

    public bool IsInvalid 
    { 
       get { return denominator == 0 && numerator != 0; }
    }

    public bool Equals(RationalNumber r)
    {
       if (r.IsZero && IsZero)
         return true;
       if (r.IsInvalid && IsInvalid)
         return true;
       return denominator == r.denominator && numerator == r.numerator;
    }

    public bool Equals(object o)
    {
       if (!(o is RationalNumber))
         return false;
       return Equals((RationalNumber)o);
    }

    public int GetHashCode()
    {
       if (IsZero) 
         return 0;
       if (IsInvalid)
         return Int32.MinValue;
       return ((float)numerator/denominator).GetHashCode();
    }
}   

Ответ 2

У вас не может быть конструктора без параметров, который присваивает значение по умолчанию. Техническая причина в том, что ваш struct является подклассом System.ValueType, а System.ValueType() - protected, поэтому его нельзя переопределить.

Ближе всего вы можете получить решение David Hefferman:

/// <summary>
/// The denominator is stored in this altered form, because all struct fields initialize to 0 - and we want newly created RationalNumbers to be 0/1 more often than 0/0.
/// </summary>
private int _denominatorMinusOne;
public int Denominator 
{ 
    get { return _denominatorMinusOne + 1; } 
    set { _denominatorMinusOne = value -1; } 
}

Тогда вы можете просто ссылаться на Denominator в свой код как обычно, а специальный формат хранения будет прозрачным - вы могли бы только сказать, посмотрев объявление поля или проверив поведение конструктора по умолчанию.

Вы можете делать такие вещи, как вызов конструктора с параметрами или создание класса RationalNumberFactory для создания нулей для вас - но ни одна из них не обошла бы вашу проблему циклирования через каждый элемент матрицы, а не только по диагонали, потому что вы не можете указать конструктор, который будет использовать инициализатор массива.

Фактически, соглашение new RationalNumber[100][100] - это не просто сокращенное кодирование, но оно также работает быстрее, чем вызов конструктора 10 000 раз. Это часть того, почему System.ValueType() было сделано protected в первую очередь. См.: Почему я не могу определить конструктор по умолчанию для структуры в .NET?

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

Ответ 3

Было бы неплохо предоставить конструктор по умолчанию для структуры:

public RationalNumber()
{
    this.numerator = 0;
    this.denominator = 1;
    this.isDefinitelyCoprime = false;
}

Однако это не разрешено. У вас также нет инициализаторов экземпляров.

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

    public struct Rational
    {
        private int _numerator;
        private int _denominator;
        public Rational(int numerator, int denominator)
        {
            // Check denominator is positive.
            if(denominator < 0){
                  denominator *= -1; 
                  numerator *= -1;
            }
            _numerator = numerator;
            _denominator = denominator== 0? -1:
                denominator;
        }
        public int Numerator
        {
            get { return _numerator; }
        }
        public int Denominator
        {
            get { return 
                _denominator == 0?1:
                _denominator == -1?0:
                _denominator; }
        }
    }

(Примечание: на самом деле я был очень удивлен, обнаружив, что у вас не могут быть статические инициализаторы в структурах!)

Ответ 4

Хорошо, когда можно проектировать структуры, чтобы любая комбинация значений поля определяла семантику. Если это не будет сделано, для структуры не будет никакого способа предотвратить построение неверно сформированных экземпляров неверно-потоковым кодом, а для таких случаев вызвать неправильное поведение в коде, который должным образом нарезается резьбой. Например, если место хранения в рациональном типе имело значения числителя и знаменателя, которые были признаны определенными взаимно простыми, и указанное местоположение было скопировано в одном потоке, а его значение было изменено в другом потоке, поток, который сделал копирование, мог получить экземпляр где числитель и знаменатель не были взаимно просты, но флаг сказал, что они были. Другой код, который получил этот экземпляр, мог потерпеть неудачу в странных и причудливых способах в результате разбитого инварианта; такой сбой может произойти где-то очень удаленно от не-потокового кода, создавшего сломанный экземпляр.

Эта ситуация может быть устранена с помощью объекта неизменяемого класса для хранения рационального числа и наличия типа значения рационального числа, который обертывает личную ссылку на такой объект. Тип-оболочка будет использовать экземпляр по умолчанию, если его личная ссылка равна NULL, или обернутый экземпляр, если это не так. Такой подход может предложить некоторые потенциальные улучшения эффективности, если частная ссылка была абстрактным типом, и было несколько производных типов, которые удовлетворяли различным критериям. Например, можно было получить производный RationalSmallInteger, единственным полем которого было Int32, а a RationalLongInteger, единственным полем которого было Int64 (свойство Denominator обоих таких типов всегда возвращало бы 1). Можно было бы иметь типы, в которых знаменатель был отличным от нуля, но был проверен как совпадающий с числителем или типами, где он не был; последний тип типа может содержать исходно-нулевую ссылку на экземпляр, где числитель и знаменатель гарантированно будут взаимно простыми. Такое поведение может повысить эффективность в таких случаях, как:

RationalNumber r1 = new RationalNumber(4,6);
RationalNumber r2 = r1;
RationalNumber r3 = r1.ReducedForm();
RationalNumber r4 = r2.ReducedForm();

Первый оператор будет устанавливать частное поле r1 для ссылки на экземпляр RationalNumber.Int32by32Nonreduced. Второй установит частное поле r2, чтобы указать на тот же самый экземпляр. Третий оператор создаст новый экземпляр Int32by32Reduced и сохранит ссылку на экземпляр в предыдущем экземпляре Int32by32Nonreduced, а также в частном поле r3. Четвертый будет брать вышеупомянутую ссылку из прежнего Int32by32Reduced и хранить ее в частном поле r4. Обратите внимание, что потребуется только одна операция сокращения. В отличие от этого, если RationalNumber была структурой, которая внутренне сохраняла свои значения, четвертое утверждение не могло бы повторно использовать результат сокращения, выполняемого третьим.