Элегантная инициализация массива экземпляров класса в С#

Скажем, у меня есть класс вроде этого:

public class Fraction
{
   int numerator;
   int denominator;

   public Fraction(int n, int d)
   {
      // set the member variables
   }

   // And then a bunch of other methods
}

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

Конечно, конструктор массива будет приятным, но нет такой вещи:

public Fraction[](params int[] numbers)

Поэтому я вынужден использовать метод типа

public static Fraction[] CreateArray(params int[] numbers)
{
    // Make an array and pull pairs of numbers for constructor calls
}

который является относительно неуклюжим, но я не вижу пути вокруг него.

Обе формы подвержены ошибкам, потому что пользователь может ошибочно передать нечетное количество параметров, возможно, потому, что он/она пропустил значение, которое оставило бы функцию, царапающую его голову, задаваясь вопросом, чего действительно хочет пользователь. Это может вызвать исключение, но тогда пользователю нужно будет попробовать/поймать. Я бы предпочел не налагать это на пользователя, если это возможно. Поэтому допустим пары.

public static Fraction[] CreateArray(params int[2][] pairs)

Но вы не можете назвать этот CreateArray красивым способом, например

Fraction.CreateArray({0,1}, {1,2}, {1,3}, {1,7}, {1,42});

Вы даже не можете сделать

public static Fraction[] CreateArray(int[2][] pairs)
// Then later...
int[2][] = {{0,1}, {1,2}, {1,3}, {1,7}, {1,42}};
Fraction.CreateArray(numDenArray);

Обратите внимание, что это отлично работает в С++ (я уверен).

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

int[2][] fracArray = {new int[2]{0,1}, /*etc*/);
Fraction.CreateArray(fracArray);
// OR
Fraction.CreateArray(new int[2]{0,1}, /*etc*/);

Аналогично, кортежи в стиле Python являются незаконными, а версия С# - icky:

Fraction.CreateArray(new Tuple<int,int>(0,1), /*etc*/);

Использование чистого 2D-массива может иметь следующую форму, но это незаконно, и я уверен, что нет законного способа его выражения:

public static Fraction[] CreateArray(int[2,] twoByXArray)
// Then later...
Fraction[] fracArray = 
    Fraction.CreateArray(new int[2,4]{{0,1}, {1,2}, {1,3}, {1,6}});

Это не обеспечивает соблюдение пар:

public static Fraction[] CreateArray(int[,] twoByXArray)

ОК, как насчет

public static Fraction[] CreateArray(int[] numerators, int[] denominators)

Но тогда два массива могут иметь разную длину. С++ позволяет

public static Fraction[] CreateArray<int N>(int[N] numerators, int[N] denominators)

но, ну, это не С++, не так ли?

Это незаконно:

public static implicit operator Fraction[](params int[2][] pairs)

и вообще не работает, опять же из-за отвратительного синтаксиса:

Fraction[] fracArray = new Fraction[](new int[2]{0,1}, /*etc*/ );

Это может быть приятно:

public static implicit operator Fraction(string s)
{
    // Parse the string into numerator and denominator with
    // delimiter '/'
}

Затем вы можете сделать

string[] fracStrings = new string[] {"0/1", /*etc*/};
Fraction[] fracArray = new Fraction[fracStrings.Length];
int index = 0;
foreach (string fracString in fracStrings) {
    fracArray[index] = fracStrings[index];
}

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

Следующее также требует расточительного экземпляра:

var fracArray = Array.ConvertAll(numDenArray, item => (Fraction)item);

Использование этого свойства имеет ту же проблему, если вы не используете эти ужасные зубчатые массивы:

public int[2] pair {
    set {
        numerator = value[0];
        denominator = value[1];
    }
}
// Then later...
var fracStrings = new int[2,4] {{0,1}, /*etc*/};
var fracArray = new Fraction[fracStrings.Length];
int index = 0;
foreach (int[2,] fracString in fracStrings) {
    fracArray[index].pair = fracStrings[index];
}

Этот вариант не обеспечивает соблюдение пар:

foreach (int[,] fracString in fracStrings) {
    fracArray[index].pair = fracStrings[index];
}

Опять же, этот подход в любом случае большой.

Это все идеи, которые я знаю, как вывести. Есть ли хорошее решение?

Ответ 1

Я не могу придумать элегантное и в то же время эффективное решение для памяти массива.

Но есть элегантное решение для списка (и аналогичного), использующее функцию С# 6 инициализатор коллекции:

public static class Extensions
{
    public static void Add(this ICollection<Fraction> target, int numerator, int denominator)
    {
        target.Add(new Fraction(numerator, denominator));
    }
}

С помощью этого метода расширения вы можете легко инициализировать список Fraction, например:

var list = new List<Fraction> { { 0, 1 }, { 1, 2 }, { 1, 3 }, { 1, 7 }, { 1, 42 } };

И, конечно, хотя и не эффективный с точки зрения памяти, вы можете использовать его для инициализации массива Fraction:

var array = new List<Fraction> { { 0, 1 }, { 1, 2 }, { 1, 3 }, { 1, 7 }, { 1, 42 } }.ToArray();

или даже сделать его более кратким, объявив класс, производный от списка, с неявным оператором преобразования массива:

public class FractionList : List<Fraction>
{
    public static implicit operator Fraction[](FractionList x) => x?.ToArray();
}

а затем используйте

Fraction[] array = new FractionList { { 0, 1 }, { 1, 2 }, { 1, 3 }, { 1, 7 }, { 1, 42 } };

Ответ 2

Вы можете создать построитель массивов дробей с плавным интерфейсом. Это приведет к чему-то вроде

public class FractionArrayBuilder
{
  private readonly List<Fraction> _fractions = new List<Fraction>();

  public FractionArrayBuilder Add(int n, int d)
  {
    _fractions.Add(new Fraction(n, d));
    return this;
  }

  public Fraction[] Build()
  {
    return _fractions.ToArray();
  }
}

который можно вызвать с помощью

var fractionArray = new FractionArrayBuilder()
  .Add(1,2)
  .Add(3,4)
  .Add(3,1)
  .Build();

который является понятным для понимания.

Я продемонстрировал fiddle.

Ответ 3

Самый краткий способ, который я могу придумать для вашего конкретного примера, заключается в написании неявного оператора для класса Fraction:

public sealed class Fraction
{
    public Fraction(int n, int d)
    {
        Numerator   = n;
        Deniminator = d;
    }

    public int Numerator   { get; }
    public int Deniminator { get; }

    public static implicit operator Fraction(int[] data)
    {
        return new Fraction(data[0], data[1]);
    }
}

Затем вы можете инициализировать его следующим образом:

var fractions = new Fraction[]
{
    new [] {1, 2},
    new [] {3, 4},
    new [] {5, 6}
};

К сожалению, вам по-прежнему нужен new [] для каждой строки, поэтому я не думаю, что это очень сильно влияет на синтаксис инициализации обычного массива:

var fractions = new []
{
    new Fraction(1, 2),
    new Fraction(3, 4),
    new Fraction(5, 6)
};

Я полагаю, вы могли бы написать "локальный" Func<> с коротким именем, чтобы немного упростить инициализацию:

Func<int, int, Fraction> f = (x, y) => new Fraction(x, y);

var fractions = new []
{
    f(1, 2),
    f(3, 4),
    f(5, 6)
};

Недостаток заключается в том, что вам нужно будет добавить эту дополнительную строку (инициализацию Func<>), где бы вы ни хотели инициализировать массив, или вместо этого иметь частный статический метод в классе, но тогда этот метод будет в области видимости во всем классе, что не является идеальным, если оно имеет однобуквенное имя.

Однако преимущество этого подхода в том, что оно очень гибкое.

Я играл с идеей вызова встроенной функции _, но я действительно не уверен в этом...

Func<int, int, Fraction> _ = (x, y) => new Fraction(x, y);

var fractions = new []
{
    _(1, 2),
    _(3, 4),
    _(5, 6)
};