继续探索C#主题,我们为您翻译了以下有关扩展方法原始用法的简短文章。我们建议您特别注意有关接口的最后部分以及作者的个人资料。
我确信任何具有一点C#经验的人都知道扩展方法的存在。这是一个很好的功能,允许开发人员使用新方法扩展现有类型。
如果要向不受控制的类型添加功能,这非常方便。实际上,任何人迟早都要为BCL编写扩展名,只是为了使事情更易于访问。
但是,除了相对明显的用例外,还有一些非常有趣的模式与扩展方法的使用直接相关,并演示了如何以非传统方式使用它们。
在枚举中添加方法
枚举仅是常量数字值的集合,每个值均分配有唯一的名称。尽管C#中的枚举从抽象类Enum继承,但它们不解释为真实类。特别是,此限制使他们无法使用方法。
在某些情况下,将逻辑编程为枚举可能会有所帮助。例如,如果枚举值可以存在于多个不同的视图中,而您想轻松地将一个转换为另一个。
例如,在典型的应用程序中设想以下类型,该类型允许您以各种格式保存文件:
public enum FileFormat
{
PlainText,
OfficeWord,
Markdown
}
该枚举定义了应用程序支持的格式列表,可用于应用程序的不同部分,以基于特定值启动分支逻辑。
由于每种文件格式都可以表示为文件扩展名,因此,如果每个文件格式
FileFormat
都有一种获取此信息的方法,那就太好了。可以通过扩展方法来完成,如下所示:
public static class FileFormatExtensions
{
public static string GetFileExtension(this FileFormat self)
{
if (self == FileFormat.PlainText)
return "txt";
if (self == FileFormat.OfficeWord)
return "docx";
if (self == FileFormat.Markdown)
return "md";
// , ,
//
throw new ArgumentOutOfRangeException(nameof(self));
}
}
反过来,这允许我们执行以下操作:
var format = FileFormat.Markdown;
var fileExt = format.GetFileExtension(); // "md"
var fileName = $"output.{fileExt}"; // "output.md"
重构模型类
有时候,您不想直接向类添加方法,例如,如果您使用的是贫血模型。
贫血模型通常由一组公共不可变属性(仅获取)表示。因此,在将方法添加到模型类中时,您可能会感到违反了代码的纯正性,或者您可能怀疑这些方法引用了某种私有状态。扩展方法不会导致此问题,因为它们无法访问模型的私有成员,并且本质上也不是模型的一部分。
因此,请考虑下面的示例,其中有两个模型,一个模型代表一个封闭的标题列表,另一个模型代表一个单独的标题行:
public class ClosedCaption
{
//
public string Text { get; }
//
public TimeSpan Offset { get; }
//
public TimeSpan Duration { get; }
public ClosedCaption(string text, TimeSpan offset, TimeSpan duration)
{
Text = text;
Offset = offset;
Duration = duration;
}
}
public class ClosedCaptionTrack
{
// ,
public string Language { get; }
//
public IReadOnlyList<ClosedCaption> Captions { get; }
public ClosedCaptionTrack(string language, IReadOnlyList<ClosedCaption> captions)
{
Language = language;
Captions = captions;
}
}
在当前状态下,如果需要获取在特定时间显示的字幕字符串,我们将像这样运行LINQ:
var time = TimeSpan.FromSeconds(67); // 1:07
var caption = track.Captions
.FirstOrDefault(cc => cc.Offset <= time && cc.Offset + cc.Duration >= time);
这确实是乞求某种可以作为成员方法或扩展方法实现的辅助方法。我更喜欢第二种选择。
public static class ClosedCaptionTrackExtensions
{
public static ClosedCaption GetByTime(this ClosedCaptionTrack self, TimeSpan time) =>
self.Captions.FirstOrDefault(cc => cc.Offset <= time && cc.Offset + cc.Duration >= time);
}
在这种情况下,扩展方法可以使您获得与通常方法相同的效果,但是会带来许多非显而易见的好处:
- 显然,此方法仅适用于该类的公共成员,并且不会以某种神秘的方式更改其私有状态。
- 显然,此方法只允许您偷工减料,此处提供此方法仅是为了方便。
- 此方法属于一个完全独立的类(甚至是汇编),其目的是将数据与逻辑分离。
通常,使用扩展方法时,在必要和有用之间划清界限是很方便的。
使接口通用
当设计接口时,您总是希望合同尽可能小,因为它使实施更容易。当界面以最通用的方式提供功能时,它会很有帮助,以便您的同事(或您自己)可以在该界面上构建以处理更具体的情况。
如果这听起来很无聊,请考虑将模型保存到文件的典型接口:
public interface IExportService
{
FileInfo SaveToFile(Model model, string filePath);
}
一切工作正常,但是可能在几周后提出新的要求:实现的类
IExportService
不仅必须导出到文件,而且还必须能够写入文件。
因此,为了满足此要求,我们在合同中添加了一种新方法:
public interface IExportService
{
FileInfo SaveToFile(Model model, string filePath);
byte[] SaveToMemory(Model model);
}
这项更改刚刚破坏了所有现有的实现
IExportService
,因为它们现在都需要更新以支持写入内存。
但是,为了不做所有这些事情,我们可以从一开始就设计界面有些不同:
public interface IExportService
{
void Save(Model model, Stream output);
}
在这种形式下,界面会强制您以最通用的形式(即this)编写目标
Stream
。现在,我们不再局限于工作时的文件,而且还可以定位其他各种输出选项。
这种方法的唯一缺点是,最基本的操作并不像我们以前那样简单:现在,我们必须设置一个特定的实例
Stream
,将其包装在using语句中并将其作为参数传递。
幸运的是,使用扩展方法时,此缺点已完全消除:
public static class ExportServiceExtensions
{
public static FileInfo SaveToFile(this IExportService self, Model model, string filePath)
{
using (var output = File.Create(filePath))
{
self.Save(model, output);
return new FileInfo(filePath);
}
}
public static byte[] SaveToMemory(this IExportService self, Model model)
{
using (var output = new MemoryStream())
{
self.Save(model, output);
return output.ToArray();
}
}
}
通过重构原始接口,我们使它更具通用性,并且没有通过使用扩展方法牺牲可用性。
因此,我发现扩展方法是保持简单简单并将复杂变为可能的宝贵工具。