C#中优雅地初始化类实例数组

37

假设我有一个像这样的类:

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);

请注意,这在C++中应该可以正常工作(我很确定)。

相反,您只能强制执行以下操作之一,这是可憎的。语法很糟糕,当所有元素具有相同长度时,使用锯齿形数组似乎非常笨拙。

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

同样地,Python风格的元组在C#中是不合法的,而且C#版本也很不好看:

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)

但是这两个数组的长度可能不同。C++允许

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

但是,好吧,这不是C++,是吗?

这种做法是非法的:

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];
}

再次强调,这个方法无论如何都很重要。

这些都是我知道的所有想法。有没有一个好的解决方案呢?


2
有一个相关帖子提出了使用对象列表的技巧。 - Axel Kemper
3个回答

40

我无法想出一种既优雅,又记忆效率高的数组解决方案。

但是,对于列表(和类似结构)有一种优雅的解决方案,可利用C#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 } };

1
我喜欢这个 List<> - Matthew Watson
5
好的解决方案!如果我们谈论代码中手写的元组,那么内存效率就不应该成为一个问题!如果超过1000个,它们应该放在资源文件中! - Falco
这需要特定版本的C#吗?我尝试了new List<Fraction> { { 0, 1 }, { 1, 2 }, [...]示例,但它说Add不接受2个参数,尽管扩展在范围内。(注意:我不是提问者) - George T
5
如果你的C#版本太古老,无法使用像这样的扩展方法Add,你可以创建一个实际的类,并将Add作为普通方法来实现。这样一来,你就不必使用List<T>了,因为它可能具有多余的功能。你可以自己创建一个类,它只需要实现IEnumerable接口和拥有一个Add方法,这两个条件是使用集合初始化语法所需的。请注意不要改变原本的意思,只需使内容更通俗易懂即可。 - Matti Virkkunen
1
.NET Fiddle 适用于 @MattiVirkkunen 的“古老”方法:https://dotnetfiddle.net/SXlNKz - Sphinxxx

8
您可以使用流畅接口创建分数数组生成器。这将导致类似以下的结果:
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();

这是一个易于理解的陈述。

我创建了一个示例以进行演示。


1
你可以将 Add 方法的第一个参数省略,并直接返回 this - Michael Rieger
1
这段代码无法编译:如果 Add() 应该是一个扩展方法,那么它需要是 static 的。但我不明白为什么它不能只是一个实例方法。 - svick
你们两个都是正确的。我已经编辑过了。 - Andy Nichols

7
我能想到最简洁的方式是为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)
};

是的,语法更好了,但仍然笨重。此外,我认为它容易出错,因为数据参数可能没有两个元素。在最坏的情况下,它只有0或1个元素,这将引发异常,潜在的异常使技术难以实现。 - MackTuesday
2
不要使用本地的 Func,我建议将其作为某个类上的 public static 方法,您可以通过 using static 访问它。 - svick
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Chef_Code

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接