C# 中的范围






4.78/5 (13投票s)
介绍 C# 中实现的范围这一知名概念。
我必须承认,尽管我非常热爱 C# 编程语言,但我有时会觉得它缺少一些东西。当这种情况发生时,我通常会选择生闷气,或者,更好的是,开始一次次地实现类似 monad 的东西,以便按照我的方式做事。这篇文章又是一个关于我想要的一个功能的叙述,这个功能叫做范围。
注意:本文没有附带源代码。事实上,这都非常琐碎。
范围?范围是什么意思?
好吧,让我简要解释一下:假设你有一个字符串,你想删除它的最后一个字符。你会怎么做?嗯,典型的解决方案是写
s.Substring(0, s.Length-1)
这在技术上是正确的,但感觉就像你用手术刀在砍僵尸(与“用链锯做脑部手术”相反)。事实上,像 Python(以及 Boo,间接)这样的某些语言允许你更简洁地表达这种切片数组的想法。具体来说,你可以使用负范围值来从数组末尾测量距离。(或者,视情况而定,从数组之后的一个元素末尾测量。)
不幸的是,默认情况下 C# 让我们很无奈,因为我们无法用像 s[0:-1]
这样漂亮的写法来替换前面的表达式。或者有办法吗?
字符串扩展
让我们稍微推测一下。假设你决定是时候用一个简单的扩展方法来结束 Substring()
的专横跋扈了,这个方法能让你输入各种奇怪的值,甚至留空
public static string Substring(this string obj, int start = 0, int end = -1)
{
if (end < 0) end = obj.Length + end;
return obj.Substring(start, end-start);
}
但这行不通,因为已经有一个名为 Substring()
的方法,它接受两个参数(尽管名称不同)。那么,我们能做什么呢?嗯,我们可以放弃并认输,将方法重命名为类似 SubString
的名字(愿 VB.NET 用户宽恕我们),或者,更好的是,尝试将范围性(或带范围性)的整个概念封装在一个单独的结构中。
范围结构
public struct Range
{
public int Start;
public int End;
}
很棒,对吧?我们现在可以在我们的 API 中使用这个东西了,我们之前写的那个奇怪的扩展方法现在可以更有意义了
public static string Substring(this string obj, Range range)
{
if (range.End < 0) range.End = obj.Length + range.End;
return obj.Substring(range.Start, range.End - range.Start);
}
然而,调用仍然很奇怪,因为我们需要写类似这样的东西
string s = "abcde";
string ss = s.Substring(new Range{Start=1,End=-2});
我相信你同意这很难看,而且不值得付出努力。我们可以将其缩小到一个集合初始化器 Range{1,-2}
,但这需要 Range
实现 IEnumerable
,这是不符合习惯的,并且通常是坏的。一定还有其他方法。
范围构造
确实有更好的方法。还记得扩展方法吗?嗯,我们可以对任何类型使用它们,包括,你猜对了,int
。所以,事不宜迟,我为你带来流畅的范围构建器
public static Range to(this int start, int end)
{
return new Range {Start = start, End = end};
}
很琐碎,对吧?现在你就有了一种方法可以快速定义一个具有奇怪负值的范围(如果你必须的话),如下所示
string ss = s.Substring(1.to(-2));
这难道不是更好吗?是的,我知道它不完美,但猜猜怎么着,我们生活在一个不完美的世界里。
就是这样吗?
这个问题的答案是哲学性的,取决于你到底想要多少。我展示了一个使用范围和 Substring
扩展的非常简单的例子,但通常,范围用于从数组/矩阵/向量中提取切片。例如,将这个概念应用于枚举,我们可以得到类似这样的东西
public static IEnumerable<T> At<T>(this IEnumerable<T> obj, Range range)
{
var list = obj.ToList();
if (range.Start < 0) range.Start += list.Count;
if (range.End < 0) range.End += list.Count;
return list.Skip(range.Start).Take(range.End - range.Start);
}
上面的代码从任何 IEnumerable
(列表、数组等)中提取一个切片,给出了提供的范围。显然,在实际场景中,对 range
参数会有更严格的检查,以及有关将整个集合具体化的明显问题。以上仅为说明目的。
不过,情况会变得更好。例如,如果上面的 range
参数实际上是 params Range[]
类型呢?那么,就像变魔术一样,你可以从一个集合中提取多个切片并将它们全部连接起来。那不是很棒吗?
但是范围最终是非常脆弱的东西,只对表示某物的开始和结束有用。如果你想,比如说,将值 0 分配给数组中的特定元素怎么办?你会创建一个 Set()
扩展方法吗?也许吧,但有一个更强大的选择。
视图
视图只是一个范围加上它所关联的对象。这个区别至关重要,因为一旦你同时拥有了范围和目标对象,你就可以做各种各样不乖的事情,比如将范围内的所有值设置为一个特定的值。让我们来充实一下这个视图的东西。首先,这个类看起来很容易理解
public class ArrayView<T>
{
public Range Range;
public T[] Array;
public ArrayView(T[] array, Range range)
{
Array = array;
if (range < 0) // you know the drill
Range = range;
}
}
我用数组作为说明,也可能是别的东西!重点是,现在我们不再有 At
方法,而是可以有一个 View
方法
public static ArrayView<T> View<T>(this T[] array, Range range)
{
return new ArrayView<T>(array, range);
}
视图作用于一个预先转换的范围(负值,还记得吗?),但它能让我们做什么呢?嗯,首先,我们可以实现一个基本的索引器
public T this[int i]
{
get {
// don't forget to pre-transform, then
return Array[Range.Start + i];
}
set {
// same here, then
Array[Range.Start + i] = value;
}
}
以下是使用方法
var a = new[] {1, 2, 3, 4};
var b = a.View(2.to(3));
b[0] = 42;
Console.WriteLine(a[2]);
是的,a[2]
确实等于 42
,正如你所期望的。
视图的乐趣
那么,你实际上可以用视图做什么呢?嗯,视图的视图怎么样?毕竟,没有什么能阻止我们再创建一个接受另一个 Range
的索引器,所以…
public ArrayView<T> this[int start, int end]
{
get
{
// pretransform, as always
return new ArrayView<T>(Array,
new Range
{
Start = start + Range.Start,
End = end + Range.End
});
}
}
是的,我实际上在该索引器中使用了两个参数来更流畅地表示范围。如果你更喜欢使用 x.to(y)
的表示法,只需创建一个重载。事实上,对于一般情况,有一个重载是个好主意。但是,设置器呢?嗯,在这种特殊情况下,唯一能救你的就是 Set()
方法,它会设置每个元素的。在这种情况下,使用 =
运算符没有多大意义。
以下是一些你可以用视图做的其他有趣的事情
实现集合操作。显然,这只在视图引用的数组类型相同时才有意义。
实现
IEnumerable
。这有点道理,这样你就可以迭代数组的视图了。
结论
在蝙蝠侠电影中的一部电影里,小丑角色说的话大致是“看看你只用汽油能制造多少混乱?”。嗯,这篇文章是扩展方法能做多少事情的另一个例子——一个不合理强大的功能,可以让你创造出具有魔法般能力的对象。
你的使用体验,不可避免地,会有所不同。但是范围可以很有用,其方式超出了本文的范围。祝你好运,并敬请期待更多关于扩展方法的不合理用法!