为什么C#的Caller Info属性需要一个默认值?

13

我刚刚发现了C# 5的Caller Info Attributes(http://msdn.microsoft.com/en-us/library/hh534540.aspx),这似乎是一个非常有用的功能。我已经阅读了一些文档 (http://www.codeproject.com/Tips/606379/Caller-Info-Attributes-in-Csharp)。不过,我想知道:为什么要传入默认值?它们如何使用?下面的示例代码展示了如何使用Caller Info Attributes:

public static void ShowCallerInfo([CallerMemberName] 
  string callerName = null, [CallerFilePath] string 
  callerFilePath = null, [CallerLineNumber] int callerLine=-1)
{
    Console.WriteLine("Caller Name: {0}", callerName);
    Console.WriteLine("Caller FilePath: {0}", callerFilePath);
    Console.WriteLine("Caller Line number: {0}", callerLine);
}

我的问题是:使用nullnull-1的默认值是什么?上面的代码与以下代码有何不同:

public static void ShowCallerInfo([CallerMemberName] 
  string callerName = "hello", [CallerFilePath] string 
  callerFilePath = "world", [CallerLineNumber] int callerLine=-42)
{
    Console.WriteLine("Caller Name: {0}", callerName);
    Console.WriteLine("Caller FilePath: {0}", callerFilePath);
    Console.WriteLine("Caller Line number: {0}", callerLine);
}
我理解这些是可选参数,编译器会提供默认值来替换我们赋的默认值。既然如此,为什么还要指定默认值?是否存在一些奇怪的边缘情况,编译器无法填充值并回退到我们提供的默认值?如果不是,那我们为什么要输入这些数据呢?让开发人员提供永远不会使用的默认值似乎相当笨拙。
免责声明:我试图在Google上搜索相关信息但没有找到任何答案。我几乎不敢在SO上问问题,因为大多数像我这样的新手问题都会受到强烈反感,但作为最后的办法,我打算冒险问一个问题。版主/高级用户,无意冒犯-在发布此帖之前,我确实尝试在其他地方寻找信息。

4
请不要将直接而简短的回复“你试了什么?”误解为敌意。尽管可能显得冷漠,但鼓励简洁明了。通过谷歌搜索结果数量可以大致判断问题是否初学者常见问题。如果你在谷歌中使用了多个关键词查询也无法找到答案,那么这个问题很可能并不是一个琐碎问题。此问题并非琐碎,我很喜欢阅读答案。 - Gusdor
4个回答

8

这些参数需要有默认值,因为Caller Info属性是使用可选参数实现的,而可选参数需要一个默认值。这样调用可以简单地用ShowCallerInfo()而不必发送任何参数,编译器会添加相关参数。

为什么一开始要使用可选参数来实现呢?这是一个更深层次的问题。他们本来可以没有,但编译器需要在实际编译之前“注入”这些参数,但与可选参数(这是C#4.0的功能)相反,它将不向后兼容,并且会破坏其他编译器/代码分析工具


我理解得对吗?开发C#的“开发人员”(主要开发人员?)出于向后兼容等原因,以有点hacky的方式实现了这个功能?旧版本的c#编译器不支持新属性(因此仍会出现错误),这样做如何帮助实现向后兼容性? - Omaer
@Omaer 如果您使用支持可选参数的旧编译器,代码就可以编译,因为有默认值。您只需不断打印默认值即可。我不会称其为黑客行为,更多地是依赖现有功能。 - i3arnon
那么你的意思是说旧编译器会忽略[CallerMemberName](和其他两个)属性,即使它们不知道这些属性的作用? - Omaer
1
@Omaer,为什么不呢?您可以创建任何属性并将其应用于参数/方法等。编译器没有理由会介意。但是这些类是 .net 4.5 的一部分,因此在使用旧框架时,您需要将它们实现为存根。 - i3arnon
好的,那么这就有意义了 - 人们仍然需要将属性实现为存根。我不确定这是否是我们的主要开发人员实现此功能的最佳方式,但我想声明一些属性存根肯定比从代码中删除所有属性要容易得多。 - Omaer

4

它们需要默认值,以便可以将参数标记为可选。如果在调用方法时没有指定参数,则编译器将为您注入正确的值,但仅在您未指定它们时才会发生。如果您这样做了,那么这些属性的“魔力”就不会发生。

据我理解,这些属性不会影响运行时,并且纯粹是为编译时而设计的,因此默认值仅用于确保参数是可选的。


2
换句话说,在被调用的方法中(应用属性的参数所在的方法),必须存在该参数。另一方面,调用者必须传递这些参数,编译器允许未指定的参数的唯一方式是给它一个默认值。
尽管属性可能影响代码生成或运行时执行,但如果移除所有属性,则源代码必须有效。因此,默认值必须在被调用方上定义,编译器仅根据应用的属性生成参数值,而不是当前在被调用方上定义的默认值。

1

其他答案提到了一些有效的用途。

他们忽略的一点是,这些本质上告诉编译器使用静态值重写对这些函数的调用。但这些值并不总是可用的。在这些情况下,编译器将不会重写调用,因此将使用默认值。

示例:

如果您使用具有这些属性的函数编译dll,并将其公开到内存中生成的脚本(例如通过Roslyn)中,那么该代码可能没有“文件名”。 有人可能会认为,生成的脚本应该使用提供的参数值调用该方法,但这意味着编译器可以静态编译的相同代码(即)即使在相同的上下文中也无法在运行时进行动态编译,这会令人困惑。 您还可以通过反射调用此方法,编译器根本不知道添加这些值。 Runtime / BCL可以构建以强制反射调用者提供这些值,但在该上下文中并没有任何有意义的文件名和行号的值。 您还可以将[CallerMemberName]添加到属性构造函数中,并将该属性应用于类。 这将没有成员名称。

在文档中查看成员名称

属性构造函数

应用于属性的方法或属性的名称。如果该属性是成员内的任何元素(例如参数、返回值或通用类型参数),则此结果是与该元素相关联的成员的名称。

没有包含的成员(例如,应用于程序集级别或类型的属性)

可选参数的默认值。

如果您想隐藏调用者信息,也可以明确提供这些值。出于某种原因。(例如,如果您使用代码混淆,这些值可能不受影响,因此在这些情况下,您可能希望提供这些值以隐藏调用者)。

请参阅文档中的备注

调用者信息值在编译时作为文字直接发出到中间语言(IL)中。与异常的StackTrace属性的结果不同,这些结果不受混淆的影响。

您可以显式地提供可选参数来控制调用者信息或隐藏调用者信息。


这对我来说比上面写的其他答案更有意义。谢谢! - Omaer

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