覆盖ToString方法还是为接口提供非ToString命名扩展方法?

4
我的问题涉及命名、设计和实现选择。我可以看到自己在如何解决问题方面有两个不同的方向,我很想看看其他可能遇到类似问题的人会如何处理这个问题。这部分是美学和功能的结合。
关于代码的一些背景... 我创建了一个称为ISlice<T>的类型,它提供了对源项目的部分引用,该源项目可以是集合(例如数组、列表)或字符串。核心支持来自几个实现类,这些实现类支持使用切片的开始和结束标记快速索引原始源中的项。目的是提供类似于Go语言提供的切片功能,同时使用Python样式的索引(即支持正数和负数索引)。
为了使切片(ISlice<T>的实例)的创建更加容易和“流畅”,我创建了一组扩展方法。例如:
static public ISlice<T> Slice<T>(this IList<T> source, int begin, int end)
{
  return new ListSlice<T>(source, begin, end);
}

static public ISlice<char> Slice(this string source, int begin, int end)
{
  return new StringSlice(source, begin, end);
}

还有其他方法,例如提供可选的开始/结束参数,但以上内容已足够满足我的需求。

这些例程很好用,可以轻松地切割集合或字符串。我还需要一种将切片复制为数组、列表或字符串的方法。这就是事情变得“有趣”的地方。最初,我认为我需要创建ToArray、ToList扩展方法,但后来想起LINQ变体如果您的集合实现了ICollection<T>,则会执行优化。在我的情况下,ISlice<T>继承了它,尽管让我感到非常不愉快,因为我不喜欢从Add等方法中抛出NotSupportedExceptions。无论如何,我免费获得了这些。太好了。

那么如何将其转换回字符串呢?由于没有内置支持将IEnumerable<char>轻松转换回字符串,所以最接近的东西是其中一个string.Concat重载,但它不能像它应该那样高效地处理字符。从设计的角度来看,同样重要的是它不会成为一个“转换”例程。

第一个想法是创建一个ToString扩展方法,但这不起作用,因为ToString是一个实例方法,这意味着它胜过扩展方法,并且永远不会被调用。我可以覆盖ToString,但行为将是不一致的,因为ListSlice<T>需要为T是char时特别处理其ToString。我不喜欢这个,因为当类型参数是char时,ToString会给出有用的东西,但在其他情况下会给出类名。此外,如果将来创建其他切片类型,则必须创建一个公共基类以确保相同的行为,否则每个类都必须实现此相同的检查。接口上的扩展方法将更加优雅地处理它。

扩展方法引导我进入了命名约定问题。显然使用ToString是最好的选择,但正如前面所述,它是不允许的。我可以给它取一个不同的名字,但是什么?ToNewString?NewString?CreateString?在To系列方法中的某个东西将使它与ToArray/ToList例程一起使用,但在智能感知和代码编辑器中,ToNewString看起来很“奇怪”。NewString/CreateString不像你必须知道要查找它们那样易于发现。它不符合To系列方法提供的“转换方法”模式。

采用覆盖ToString并接受硬编码到ListSlice<T>实现和其他实现中的不一致行为?采用更灵活但命名可能更差的扩展方法路线?有我没有考虑过的第三种选择吗?

我的直觉告诉我要采用ToString,尽管我对此有所保留,但它也让我想到了...您是否会考虑ToString在集合/可枚举类型上给您提供有用的输出?那是否违反了最小惊讶原则?

更新

大多数切片操作的实现都提供了数据的副本,尽管是一个子集,来自用于该切片的任何源。这在大多数情况下是完全可以接受的,并且使得 API 更加清晰简洁,因为你可以简单地返回相同的数据类型。如果你切片一个列表,你会返回一个仅包含指定范围内项目的列表。如果你切片一个字符串,你会返回一个字符串。以此类推。
上述我所描述的切片操作解决了在使用约束时产生的问题,这种行为是不可取的。例如,如果你处理大型数据集,切片操作将导致不必要的额外内存分配,更不用说复制数据的性能影响了。特别是在切片在到达最终结果之前需要进一步处理的情况下,这一点尤其明显。因此,切片实现的目标是引用较大数据集中的数据,以避免在没有必要的情况下复制信息,直到有益为止。
问题在于,在处理结束时,希望将基于切片的处理数据转换回更易于传递到其他 API 的 API 和 .NET 友好类型,如列表、数组和字符串。它还允许你丢弃切片,从而也丢弃了切片所引用的大型数据集。

2
FYI,添加反引号符号将允许您使用 IEnumerable<char> 这样的语句。 - StriplingWarrior
谢谢你提醒我。我错过了相关的代码插入技巧。 - James Arendt
2个回答

5

你会考虑ToString在集合/可枚举类型上给出有用的输出吗?这会违反最小惊奇原则吗?

不会,也会。这将是完全意外的行为,因为它会与每个其他集合类型的行为不同。

至于这个:

那么将其转换回字符串,因为没有内置支持轻松将IEnumerable>char<转换回字符串的方法呢?

就我个人而言,我会使用接受数组的字符串构造函数

string result = new string(mySlice.ToArray());

我希望您能够理解并预期这一点——通过将对象传递给构造函数,我希望创建一个新的字符串。

我已经考虑过这个问题,但是我的切片实现的目标是尽量减少分配内存,从我目前对字符串构造函数的理解来看,上述方法会导致切片数据进行两次复制。一次是将数据复制到数组中,然后字符串会对传入的数组进行防御性复制。对于较小的工作集,这是一个可能的选择。我更新了我的帖子,分享了更多有关我的切片实现假设的信息。 - James Arendt
@James:你打算如何不同地实现你的ToString()方法?最终你需要生成一个字符串,并复制数据... - Reed Copsey
我原本打算使用StringBuilder来构建字符串,然后调用ToString()方法。今天早上进行了一些性能比较,与数组/构造函数方法相比结果不尽如人意。我一直以为SB是在写时复制的。我是对的,它确实是这样的。发现在4.0中行为发生了变化。你的方法不仅是正确的惯用方法,而且在这种情况下也是最好的方法。谢谢。 - James Arendt

1
也许你遇到的难题是因为你将 string 当作了一个 ICollection<char> 来处理。你没有提供关于你试图解决的问题的细节,但也许这是一个错误的假设。
确实,字符串是一个 IEnumerable<char>。但正如你已经注意到的那样,假设直接映射到字符集合会创建问题。在框架中,字符串太“特殊”了。
从另一方面来看,一个 ISlice<char>ISlice<byte> 之间的区别是否很明显,前者可以连接成一个字符串?后者是否有一个合理的连接操作?ISlice<string> 呢?我不应该也能连接它们吗?
抱歉我没有提供具体的答案,但也许这些问题会指引你找到正确的解决方案。

我更新了我的帖子,并在我的实现中加入了更多假设。但是,你提出了另一个问题,即关于具有“concat”类型操作的想法,我没有表达。我一直在考虑的一个可能的未来方向是“组合”操作,它允许您从两个或多个切片中构建一个更大的单元。它本质上是一个切片连接操作,可以产生一个更大的“切片”。这与转换为字符串所做的不同,后者是复制字符数据以成为切片的副本,但采用.NET友好的字符串格式。 - James Arendt

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