在 C# 中优雅地初始化类实例数组
本文关键字:初始化 实例 数组 | 更新日期: 2024-10-20 22:03:35
假设我有一个这样的类:
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# 版本也很糟糕:
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];
}
同样,无论如何,这种方法都很大。
这些都是我知道如何推导出的想法。有没有好的解决方案?
我想不出一个优雅的,同时又有内存效率的数组解决方案。
但是,对于使用 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 } };
您可以创建一个具有流畅界面的分数数组生成器。它会导致类似
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();
这是一个易于理解的说法。
我做了一个小提琴来演示。
对于您的特定示例,我能想到的最简洁的方法是为 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)
};