5.2 方法的声明
本节描述如何声明包含参数或返回类型的方法。代码清单5.4演示了这些概念,输出5.1展示了结果。
代码清单5.4 声明方法
输出5.1
代码清单5.4声明了5个方法。从Main()中调用了GetUserInput(),然后调用GetFullName()和GetInitials()。后三个方法都返回一个值,而且都要获取实参。最后调用DisplayGreeting(),它不返回任何数据。C#的每个方法都必须在某个类型中,本例的包容类型是IntroducingMethods类。即使第1章讨论的Main()方法也必须在一个类型中。
语言对比:C++/Visual Basic——全局方法
C#不支持全局方法,一切都必须在类型声明中。这正是Main()方法标记为static的原因——它等价于C++的全局方法和Visual Basic的“共享”方法。
初学者主题:用方法进行重构
将一组语句转移到一个方法中,而不是把它们留在一个较大的方法中,这是一种重构形式。重构有助于减少重复代码,因为可从多个位置调用方法,而不必在每个位置都重复这些代码。重构还有助于增强代码的可读性。编码时的一个最佳实践是经常检查代码,找出可重构的地方。尤其是那些不好理解的代码块,最好把它们转移到方法中,用有意义的方法名清晰定义代码的行为。与简单地为代码块加上注释相比,重构效果更好,因为看方法名就知道方法要做的事情。
例如,代码清单5.4的Main()方法具有与第1章代码清单1.16中的Main()方法差不多的行为。虽然两者都容易看懂,但代码清单5.4更简洁。只需扫一眼Main()方法就可理解该程序(暂时不用关心被调用的每个方法的实现细节)。
在Visual Studio中可选定一组语句,右击并选择“快速操作和重构”(Ctrl+.)菜单命令,然后在弹出的小窗口中选择“提取方法”命令,将该块提取到其自己的方法中,自动插入代码以从原始位置调用新方法。
5.2.1 参数声明
注意DisplayGreeting()、GetFullName()和GetInitials()方法的声明。可在方法声明的圆括号中添加参数列表(讨论泛型时会讲到,方法也可以有类型参数列表。如根据上下文能分清当前讲的是哪种参数,就直接把它们称为“参数列表”中的“参数”)。列表中的每个参数都包含参数类型和参数名称,每个参数以逗号分隔。
大多数参数的行为和命名规范与局部变量一致。因此参数名采用camelCase大小写形式。另外,不能在方法中声明与参数同名的局部变量,因为这会创建同名的两个“局部变量”。
设计规范
·要为参数名使用camelCase大小写。
5.2.2 方法返回类型声明
GetUserInput()、GetFullName()和GetInitials()方法除了定义参数,还定义了方法返回类型。很容易就可分辨一个方法是否有返回值,因为在声明这种方法时,会在方法名之前添加一个数据类型。上述所有方法的返回数据类型都是string。虽然方法可指定多个参数,但返回类型只能有一个。
如GetUserInput()和GetInitials()方法所示,具有返回类型的方法几乎总是包含一个或多个return语句将控制返回给调用者。return语句以return关键字开头,后跟计算返回值的表达式。例如,GetInitials()方法的return语句是return $"{firstName[0]}.{lastName[0]}.";。return关键字后面的表达式必须兼容方法的返回类型。
如果方法有返回类型,它的主体必须没有“不可到达的结束点”。换言之,方法不能在不返回值的情况下碰到大括号而自然结束。为保证这一点,最简单的办法就是将return语句作为方法的最后一个语句。但这并非绝对,return语句并非只能在方法末尾出现。例如,方法中的if或switch语句可以包含return语句,如代码清单5.5所示。
代码清单5.5 方法中间的return语句
(注意,return语句将控制转移出switch,所以不需要用break语句防止非法“贯穿”switch部分。)
在代码清单5.5中,方法最后一个语句不是return语句,而是switch语句。但编译器判断方法的每条执行路径最终都是return语句,所以方法结束点“不可到达”。这样的方法是合法的,即使它不以return语句结尾。
如果return之后有“不可到达”的语句,编译器会发出警告,指出有永远执行不到的语句。
虽然C#允许提前返回,但为了增强代码的可读性,并使代码更易维护,应尽量确定单一的退出位置,而不是在方法的多个代码路径中散布多个return语句。
指定void作为返回类型表示该方法没有返回值。所以,这种方法不支持向变量赋值,也无法在调用位置[1]作为参数传递。void调用只能作为语句使用。此外,return在这种方法内部可选。如指定return,它之后不能有任何值。例如代码清单5.4的Main()方法的返回值是void,方法中没有使用return语句。而DisplayGreeting()有return语句,但return之后没有添加任何值。
虽然从技术上说方法只能有一个返回类型,但返回类型可以是一个元组。从C# 7.0起,多个值可通过C#元组语法打包成元组返回,如代码清单5.6的GetName()方法所示。
代码清单5.6 用元组返回多个值
技术上仍然只返回一个数据类型,即一个ValueTuple<string, string>,但实际可以返回任意数量(当然要合理)。
5.2.3 表达式主体方法
有些方法过于简单。为简化这些方法的定义,C# 6.0引入了表达式主体方法,允许用表达式代替完整方法主体。代码清单5.4的GetFullName()方法就是一例:
表达式主体方法不是用大括号定义方法主体,而是用=>操作符(第13章详述)。该操作符的结果数据类型必须与方法返回类型匹配。换言之,虽然没有显式的return语句,但表达式本身的返回类型必须与方法声明的返回类型匹配。
表达式主体方法是大量方法主体声明的语法快捷方法。正因如此,其应用应限于最简单的方法实现,例如单行表达式。
语言对比:C++——头文件
和C++不同,C#类从来不将实现与声明分开。C#不区分头文件(.h)和实现文件(.cpp)。相反,声明和实现在同一个文件中。(C#确实支持名为“分部方法”的高级功能,允许将方法的声明和实现分开。但考虑到本章的目的,我们只讨论非分部方法。)这样就不需要在两个位置维护冗余的声明信息。
初学者主题:命名空间
如前所述,命名空间是分类和分组相关类型的一种机制。在一个类型所在的命名空间中,能找到和它相关的其他类型。此外,不同命名空间中重名的两个或更多类型没有歧义。
[1] call site,就是发出调用的地方,可理解成调用了一个目标方法的表达式或代码行。——译者注