链接:msdn.microsoft.com/magazine/mt790184
解构函数
从 C# 1.0 开始,就能调用函数,就是将参数组合起来并封装到一个类中的构造函数。但是,从来没有一种简便的方式可将对象解构回其各个组成部分。例如,假设有一个 PathInfo 类,它采用文件名的每个元素(目录名、文件名、扩展名),并将它们组合成一个对象,然后支持操作对象的不同元素。现在,假设你需要将该对象提取(解构)回其各个组成部分。
在 C# 7.0 中,通过解构函数完成这项任务将变得轻而易举,解构函数可返回对象的具体确定组件。注意,不要将解构函数 (deconstructor) 与析构函数 (destructor)(确定性对象解除分配和清除)或终结器 (itl.tc/CSharpFinalizers) 混淆。
我们来看看图 1 中的 PathInfo 类。
图 1 具有解构函数的 PathInfo 类及相关测试
public class PathInfo
{
public string DirectoryName { get; }
public string FileName { get; }
public string Extension { get; }
public string Path
{
get
{
return System.IO.Path.Combine(
DirectoryName, FileName, Extension);
}
}
public PathInfo(string path)
{
DirectoryName = System.IO.Path.GetDirectoryName(path);
FileName = System.IO.Path.GetFileNameWithoutExtension(path);
Extension = System.IO.Path.GetExtension(path);
}
public void Deconstruct(
out string directoryName, out string fileName, out string extension)
{
directoryName = DirectoryName;
fileName = FileName;
extension = Extension;
}
// ...
}
显然,可以和在 C# 1.0 一样调用 Deconstruct 方法。但是,C# 7.0 提供了可以显著简化调用的语法糖。如果存在解构函数的声明,则可以使用新的 C# 7.0“类似元组”的语法调用它(参见图 2)。
图 2 解构函数调用和赋值
PathInfo pathInfo = new PathInfo(@"\\test\unc\path\to\something.ext");
{
// Example 1: Deconstructing declaration and assignment.
(string directoryName, string fileName, string extension) = pathInfo;
VerifyExpectedValue(directoryName, fileName, extension);
}
{
string directoryName, fileName, extension = null;
// Example 2: Deconstructing assignment.
(directoryName, fileName, extension) = pathInfo;
VerifyExpectedValue(directoryName, fileName, extension);
}
{
// Example 3: Deconstructing declaration and assignment with var.
var (directoryName, fileName, extension) = pathInfo;
VerifyExpectedValue(directoryName, fileName, extension);
}
请注意,C# 第一次如何允许同时向不同值的多个变量赋值。这与将所有变量都初始化为同一值 (null) 的空赋值声明不同:
string directoryName, filename, extension = null;
通过新的类似元组的语法,赋予每个变量一个不同的值,该值与其名称不对应,但与它出现在声明和解构语句中的顺序相对应。
正如你所期望的,out 参数的类型必须与被分配的变量类型相匹配,并且允许使用 var,因为此类型可以从 Deconstruct 参数类型中推断出来。但是,请注意,虽然可以在圆括号外面放置一个 var(如图 2 中的示例 3 所示),但此时即使所有变量的类型均相同,也不能拉出字符串。
请注意,此时 C# 7.0 类似元组的语法要求圆括号内至少出现两个变量。例如,即使存在类似如下的解构函数,也不允许使用 (FileInfo path) = pathInfo;:
public void Deconstruct(out FileInfo file)
换句话说,不能对仅有一个 out 参数的 Deconstruct 方法使用 C# 7.0 解构函数。
使用元组
正如我所说过的,前面的每个示例都利用了 C# 7.0 类似元组的语法。此类语法的特点就是用圆括号括住分配的多个变量(或属性)。我之所以使用术语“类似元组的”,是因为所有这些解构函数示例实际上在内部均未使用任何元组类型。(实际上,由于已分配的对象是表示封装的组成部分的实例,因此,不允许通过解构函数语法分配元组,也可以说这样做不太必要。)
借助 C# 7.0,现在有了一种特别简化的语法,可以使用元组,如图 3 所示。只要允许使用类型说明符,就可以使用这种语法,其中包括声明、强制转换运算符和类型参数。
图 3 声明、实例化并使用 C# 7.0 元组语法
[TestMethod]
public void Constructor_CreateTuple()
{
(string DirectoryName, string FileName, string Extension) pathData =
(DirectoryName: @"\\test\unc\path\to",
FileName: "something",
Extension: ".ext");
Assert.AreEqual<string>(
@"\\test\unc\path\to", pathData.DirectoryName);
Assert.AreEqual<string>(
"something", pathData.FileName);
Assert.AreEqual<string>(
".ext", pathData.Extension);
Assert.AreEqual<(string DirectoryName, string FileName, string Extension)>(
(DirectoryName: @"\\test\unc\path\to",
FileName: "something", Extension: ".ext"),
(pathData));
Assert.AreEqual<(string DirectoryName, string FileName, string Extension)>(
(@"\\test\unc\path\to", "something", ".ext"),
(pathData));
Assert.AreEqual<(string, string, string)>(
(@"\\test\unc\path\to", "something", ".ext"), (pathData));
Assert.AreEqual<Type>(
typeof(ValueTuple<string, string, string>), pathData.GetType());
}
[TestMethod]
public void ValueTuple_GivenNamedTuple_ItemXHasSameValuesAsNames()
{
var normalizedPath =
(DirectoryName: @"\\test\unc\path\to", FileName: "something",
Extension: ".ext");
Assert.AreEqual<string>(normalizedPath.Item1, normalizedPath.DirectoryName);
Assert.AreEqual<string>(normalizedPath.Item2, normalizedPath.FileName);
Assert.AreEqual<string>(normalizedPath.Item3, normalizedPath.Extension);
}
static public (string DirectoryName, string FileName, string Extension)
SplitPath(string path)
{
// See Normalize method for full implementation.
return (
System.IO.Path.GetDirectoryName(path),
System.IO.Path.GetFileNameWithoutExtension(path),
System.IO.Path.GetExtension(path)
);
}