C#类成员 – 方法
“方法”是包含一系列语句的代码块。程序通过“调用”方法并指定所需的任何方法参数来执行语句。在 C# 中,每个执行指令都是在方法的上下文中执行的。Main 方法是每个 C# 应用程序的入口点,在启动程序时由公共语言运行时 (CLR) 调用。
1、方法签名
通过指定方法的访问级别(例如 public 或 private)、可选修饰符(例如 abstract 或 sealed)、返回值、名称和任何方法参数,可以在类或结构中声明方法。这些部分统称为方法的“签名”。
为进行方法重载,方法的返回类型不是方法签名的一部分。但是,在确定委托和委托所指向方法之间的兼容性时,返回类型是方法签名的一部分。
方法参数括在括号中,并用逗号隔开。空括号表示方法不需要参数。下面的类包含三个方法:
abstract class Motorcycle
{
// Anyone can call this.
public void StartEngine() {/* Method statements here */ }
// Only derived classes can call this.
protected void AddGas(int gallons) { /* Method statements here */ }
// Derived classes can override the base class implementation.
public virtual int Drive(int miles, int speed) { /* Method statements here */ return 1; }
// Derived classes must implement this.
public abstract double GetTopSpeed();
}
2、方法访问
在对象上调用方法类似于访问字段。在对象名称之后,依次添加句点、方法名称和括号。参数在括号内列出,并用逗号隔开。因此,可以按以下示例中的方式调用 Motorcycle 类的方法:
class TestMotorcycle : Motorcycle
{
public override double GetTopSpeed()
{
return 108.4;
}
static void Main()
{
TestMotorcycle moto = new TestMotorcycle();
moto.StartEngine();
moto.AddGas(15);
moto.Drive(5, 20);
double speed = moto.GetTopSpeed();
Console.WriteLine("My top speed is {0}", speed);
}
}
3、方法形参和实参
方法定义指定所需任何“形参”的名称和类型。调用代码在调用方法时,将为每个形参提供称为“实参”的具体值。实参必须与形参类型兼容,但调用代码中使用的实参名称(如果有)不必与方法中定义的形参名称相同。例如:
public void Caller()
{
int numA = 4;
// Call with an int variable.
int productA = Square(numA);
int numB = 32;
// Call with another int variable.
int productB = Square(numB);
// Call with an integer literal.
int productC = Square(12);
// Call with an expression that evaulates to int.
productC = Square(productA * 3);
}
int Square(int i)
{
// Store input argument in a local variable.
int input = i;
return input * input;
}
4、通过引用传递与通过值传递
默认情况下,将值类型传递给方法时,传递的是副本而不是对象本身。因此,对参数所做的更改对于调用方法中的原始副本没有影响。可以使用 ref 关键字通过引用传递值类型。
“引用类型”通过引用进行传递。将引用类型的对象传递给方法时,引用指向原始对象而不是副本。因此,通过此引用所进行的更改将反映在调用方法中。引用类型是通过使用 class 关键字创建的,如下面的示例中所示:
public class SampleRefType
{
public int value;
}
现在,如果将基于此类型的对象传递给方法,则会通过引用传递该对象。例如:
public static void TestRefType()
{
SampleRefType rt = new SampleRefType();
rt.value = 44;
ModifyObject(rt);
Console.WriteLine(rt.value);
}
static void ModifyObject(SampleRefType obj)
{
obj.value = 33;
}
此示例的效果本质上与前一示例相同。但是,由于使用的是引用类型,因此 ModifyObject 所做的更改反映在 TestRefType 方法中创建的对象中。因此,TestRefType 方法将显示值 33。
5、返回值
方法可以向调用方返回值。如果返回类型(方法名称前列出的类型)不是 void,则方法可以使用 return 关键字来返回值。如果语句中 return 关键字的后面是与返回类型匹配的值,则该语句将该值返回给方法调用方。return 关键字还会停止方法的执行。如果返回类型为 void,则可使用没有值的 return 语句来停止方法的执行。如果没有 return 关键字,方法执行到代码块末尾时即会停止。具有非 void 返回类型的方法才能使用 return 关键字返回值。例如,下面的两个方法使用 return 关键字来返回整数:
class SimpleMath
{
public int AddTwoNumbers(int number1, int number2)
{
return number1 + number2;
}
public int SquareANumber(int number)
{
return number * number;
}
}
若要使用从方法返回的值,调用方法可以在本来使用同一类型的值就已足够的任何位置使用方法调用本身。还可以将返回值赋给变量:例如,下面的两个代码示例可实现相同的目的:
int result = obj.AddTwoNumbers(1, 2);
obj.SquareANumber(result);
obj.SquareANumber(obj.AddTwoNumbers(1, 2));
可以选择使用局部变量(本例中为 result)来存储值。这有助于提高代码的可读性,并且如果要为方法的整个范围存储参数的原始值,可能必须这样做。
一、传递参数
在 C# 中,既可以通过值也可以通过引用传递参数。通过引用传递参数允许函数成员(方法、属性、索引器、运算符和构造函数)更改参数的值,并保持该更改。若要通过引用传递参数,请使用 ref 或 out 关键字。
// Passing by value
static void Square(int x)
{
// code...
}
// Passing by reference
static void Square(ref int x)
{
// code...
}
·传递值参数
向方法传递值类型变量意味着向方法传递变量的一个副本,方法内发生的对参数的更改对该变量中存储的原始数据无任何影响:
下面的示例演示通过值传递值类型参数。通过值将变量 n 传递给方法 SquareIt。方法内发生的任何更改对变量的原始值无任何影响
class PassingValByVal
{
static void SquareIt(int x)
// The parameter x is passed by value.
// Changes to x will not affect the original value of x.
{
x *= x;
System.Console.WriteLine("The value inside the method: {0}", x);
}
static void Main()
{
int n = 5;
System.Console.WriteLine("The value before calling the method: {0}", n);
SquareIt(n); // Passing the variable by value.
System.Console.WriteLine("The value after calling the method: {0}", n);
// Keep the console window open in debug mode.
System.Console.WriteLine("Press any key to exit.");
System.Console.ReadKey();
}
}
/* Output:
The value before calling the method: 5
The value inside the method: 25
The value after calling the method: 5
*/
变量 n 为值类型,包含其数据(值为 5)。当调用 SquareIt 时,n 的内容被复制到参数 x 中,在方法内将该参数求平方。但在 Main 中,n 的值在调用 SquareIt 方法前后是相同的。实际上,方法内发生的更改只影响局部变量 x。
下面的示例除使用 ref 关键字传递参数以外,其余与上一示例相同。参数的值在调用方法后发生更改。
class PassingValByRef
{
static void SquareIt(ref int x)
// The parameter x is passed by reference.
// Changes to x will affect the original value of x.
{
x *= x;
System.Console.WriteLine("The value inside the method: {0}", x);
}
static void Main()
{
int n = 5;
System.Console.WriteLine("The value before calling the method: {0}", n);
SquareIt(ref n); // Passing the variable by reference.
System.Console.WriteLine("The value after calling the method: {0}", n);
// Keep the console window open in debug mode.
System.Console.WriteLine("Press any key to exit.");
System.Console.ReadKey();
}
}
/* Output:
The value before calling the method: 5
The value inside the method: 25
The value after calling the method: 25
*/
本示例中,传递的不是 n 的值,而是对 n 的引用。参数 x 不是 int 类型,它是对 int 的引用(本例中为对 n 的引用)。因此,当在方法内对 x 求平方时,实际被求平方的是 x 所引用的项:n。
更改所传递参数的值的常见示例是 Swap 方法,在该方法中传递 x 和 y 两个变量,然后使方法交换它们的内容。必须通过引用向 Swap 方法传递参数;否则,方法内所处理的将是参数的本地副本。以下是使用引用参数的 Swap 方法的示例:
static void SwapByRef(ref int x, ref int y)
{
int temp = x;
x = y;
y = temp;
}
调用该方法时,请在调用中使用 ref 关键字,如下所示:
static void Main()
{
int i = 2, j = 3;
System.Console.WriteLine("i = {0} j = {1}" , i, j);
SwapByRef (ref i, ref j);
System.Console.WriteLine("i = {0} j = {1}" , i, j);
// Keep the console window open in debug mode.
System.Console.WriteLine("Press any key to exit.");
System.Console.ReadKey();
}
/* Output:
i = 2 j = 3
i = 3 j = 2
*/
·传递引用参数
引用类型的变量不直接包含其数据;它包含的是对其数据的引用。使用 ref 或 out 关键字传递参数,方法内发生的对参数的更改对该变量中存储的原始数据产生影响。
下面的示例演示通过值向 Change 方法传递引用类型的参数 arr。由于该参数是对 arr 的引用,所以有可能更改数组元素的值。但是,试图将参数重新分配到不同的内存位置时,该操作仅在方法内有效,并不影响原始变量 arr。
class PassingRefByVal
{
static void Change(int[] pArray)
{
pArray[0] = 888; // This change affects the original element.
pArray = new int[5] {-3, -1, -2, -3, -4}; // This change is local.
System.Console.WriteLine("Inside the method, the first element is: {0}", pArray[0]);
}
static void Main()
{
int[] arr = {1, 4, 5};
System.Console.WriteLine("Inside Main, before calling the method, the first element is: {0}", arr [0]);
Change(arr);
System.Console.WriteLine("Inside Main, after calling the method, the first element is: {0}", arr [0]);
}
}
/* Output:
Inside Main, before calling the method, the first element is: 1
Inside the method, the first element is: -3
Inside Main, after calling the method, the first element is: 888
*/
在上个示例中,数组 arr 为引用类型,在未使用 ref 参数的情况下传递给方法。在此情况下,将向方法传递指向 arr 的引用的一个副本。输出显示方法有可能更改数组元素的内容,在这种情况下,从 1 改为 888。但是,在 Change 方法内使用 new 运算符来分配新的内存部分,将使变量 pArray 引用新的数组。因此,这之后的任何更改都不会影响原始数组 arr(它是在 Main 内创建的)。实际上,本示例中创建了两个数组,一个在 Main 内,一个在 Change 方法内。
本示例除在方法头和调用中使用 ref 关键字以外,其余与上个示例相同。方法内发生的任何更改都会影响调用程序中的原始变量
class PassingRefByRef
{
static void Change(ref int[] pArray)
{
// Both of the following changes will affect the original variables:
pArray[0] = 888;
pArray = new int[5] {-3, -1, -2, -3, -4};
System.Console.WriteLine("Inside the method, the first element is: {0}", pArray[0]);
}
static void Main()
{
int[] arr = {1, 4, 5};
System.Console.WriteLine("Inside Main, before calling the method, the first element is: {0}", arr[0]);
Change(ref arr);
System.Console.WriteLine("Inside Main, after calling the method, the first element is: {0}", arr[0]);
}
}
/* Output:
Inside Main, before calling the method, the first element is: 1
Inside the method, the first element is: -3
Inside Main, after calling the method, the first element is: -3
*/
方法内发生的所有更改都影响 Main 中的原始数组。实际上,使用 new 运算符对原始数组进行了重新分配。因此,调用 Change 方法后,对 arr 的任何引用都将指向 Change 方法中创建的五个元素的数组。
交换字符串是通过引用传递引用类型参数的很好的示例。本示例中,str1 和 str2 两个字符串在 Main 中初始化,并作为由 ref 关键字修改的参数传递给 SwapStrings 方法。这两个字符串在该方法内以及 Main 内均进行交换。
class SwappingStrings
{
static void SwapStrings(ref string s1, ref string s2)
// The string parameter is passed by reference.
// Any changes on parameters will affect the original variables.
{
string temp = s1;
s1 = s2;
s2 = temp;
System.Console.WriteLine("Inside the method: {0} {1}", s1, s2);
}
static void Main()
{
string str1 = "John";
string str2 = "Smith";
System.Console.WriteLine("Inside Main, before swapping: {0} {1}", str1, str2);
SwapStrings(ref str1, ref str2); // Passing strings by reference
System.Console.WriteLine("Inside Main, after swapping: {0} {1}", str1, str2);
}
}
/* Output:
Inside Main, before swapping: John Smith
Inside the method: Smith John
Inside Main, after swapping: Smith John
*/
本示例中,需要通过引用传递参数以影响调用程序中的变量。如果同时从方法头和方法调用中移除 ref 关键字,则调用程序中不会发生任何更改。
·如何:了解向方法传递结构和向方法传递类引用之间的区别
本示例演示在向方法传递结构时,传递的是该结构的副本,而在传递类实例时,传递的是一个引用。
本示例的输出表明:当向 ClassTaker 方法传递类实例时,只更改类字段的值。但是向 StructTaker 方法传递结构实例并不更改结构字段。这是因为向 StructTaker 方法传递的是结构的副本,而向 ClassTaker 方法传递的是对类的引用。
class TheClass
{
public string willIChange;
}
struct TheStruct
{
public string willIChange;
}
class TestClassAndStruct
{
static void ClassTaker(TheClass c)
{
c.willIChange = "Changed";
}
static void StructTaker(TheStruct s)
{
s.willIChange = "Changed";
}
static void Main()
{
TheClass testClass = new TheClass();
TheStruct testStruct = new TheStruct();
testClass.willIChange = "Not Changed";
testStruct.willIChange = "Not Changed";
ClassTaker(testClass);
StructTaker(testStruct);
Console.WriteLine("Class field = {0}", testClass.willIChange);
Console.WriteLine("Struct field = {0}", testStruct.willIChange);
// Keep the console window open in debug mode.
Console.WriteLine("Press any key to exit.");
Console.ReadKey();
}
}
/* Output:
Class field = Changed
Struct field = Not Changed
*/
二、扩展方法
扩展方法使您能够向现有类型“添加”方法,而无需创建新的派生类型、重新编译或以其他方式修改原始类型。扩展方法是一种特殊的静态方法,但可以像扩展类型上的实例方法一样进行调用。对于用 C# 和 Visual Basic 编写的客户端代码,调用扩展方法与调用在类型中实际定义的方法之间没有明显的差异。
最常见的扩展方法是 LINQ 标准查询运算符,这些运算符在现有 System.Collections..::.IEnumerable 和 System.Collections.Generic..::.IEnumerable<(Of <(T>)>) 类型中添加了查询功能。若要使用这些标准查询运算符,请先使用 using System.Linq 指令将它们纳入范围中。然后,任何实现了 IEnumerable<(Of <(T>)>) 的类型看起来都具有 GroupBy、OrderBy、Average 等实例方法。在 IEnumerable<(Of <(T>)>) 类型的实例(如 List<(Of <(T>)>) 或 Array)后键入“点”时,可以在 IntelliSense 语句结束中看到这些附加方法。
下面的示例演示如何对一个整数数组调用标准查询运算符 OrderBy 方法。括号里面的表达式是一个 lambda 表达式。很多标准查询运算符采用 lambda 表达式作为参数,但这不是扩展方法的必要条件。
class ExtensionMethods2
{
static void Main()
{
int[] ints = { 10, 45, 15, 39, 21, 26 };
var result = ints.OrderBy(g => g);
foreach (var i in result)
{
System.Console.Write(i + " ");
}
}
}
//Output: 10 15 21 26 39 45
扩展方法被定义为静态方法,但它们是通过实例方法语法进行调用的。它们的第一个参数指定该方法作用于哪个类型,并且该参数以 this 修饰符为前缀。仅当您使用 using 指令将命名空间显式导入到源代码中之后,扩展方法才位于范围中。
下面的示例演示为 System..::.String 类定义的一个扩展方法。请注意,它是在非嵌套、非泛型静态类内部定义的:
namespace ExtensionMethods
{
public static class MyExtensions
{
public static int WordCount(this String str)
{
return str.Split(new char[] { ' ', '.', '?' }, StringSplitOptions.RemoveEmptyEntries).Length;
}
}
}
可使用以下 using 指令将 WordCount 扩展方法放入范围中:
using ExtensionMethods;
而且,可以在应用程序中使用以下语法对该扩展方法进行调用:
string s = "Hello Extension Methods";
int i = s.WordCount();
在代码中,可以使用实例方法语法调用该扩展方法。但是,编译器生成的中间语言 (IL) 会将代码转换为对静态方法的调用。因此,并未真正违反封装原则。实际上,扩展方法无法访问它们所扩展的类型中的私有变量。
通常,您更多时候是调用扩展方法而不是实现您自己的扩展方法。由于扩展方法是使用实例方法语法调用的,因此不需要任何特殊知识即可从客户端代码中使用它们。若要为特定类型启用扩展方法,只需为在其中定义这些方法的命名空间添加 using 指令。例如,若要使用标准查询运算符,请将以下 using 指令添加到代码中:
using System.Linq;
(您可能还必须添加对 System.Core.dll 的引用。)您将注意到,标准查询运算符现在作为可供大多数 IEnumerable<(Of <(T>)>) 类型使用的附加方法显示在 IntelliSense 中。
注意:尽管标准查询运算符没有显示在 String 的 IntelliSense 中,但它们仍然可用。
在编译时绑定扩展方法
可以使用扩展方法来扩展类或接口,但不能重写扩展方法。与接口或类方法具有相同名称和签名的扩展方法永远不会被调用。编译时,扩展方法的优先级总是比类型本身中定义的实例方法低。换句话说,如果某个类型具有一个名为 Process(int i) 的方法,而您有一个具有相同签名的扩展方法,则编译器总是绑定到该实例方法。当编译器遇到方法调用时,它首先在该类型的实例方法中寻找匹配的方法。如果未找到任何匹配方法,编译器将搜索为该类型定义的任何扩展方法,并且绑定到它找到的第一个扩展方法。下面的示例演示编译器如何确定要绑定到哪个扩展方法或实例方法。
示例
下面的示例演示 C# 编译器在确定是将方法调用绑定到类型上的实例方法还是绑定到扩展方法时所遵循的规则。静态类 Extensions 包含为任何实现了 IMyInterface 的类型定义的扩展方法。类 A、B 和 C 都实现了该接口。
MethodB 方法永远不会被调用,因为它的名称和签名与这些类已经实现的方法完全匹配。
如果编译器找不到具有匹配签名的实例方法,它会绑定到匹配的扩展方法(如果存在这样的方法)。
namespace Extensions
{
using System;
using ExtensionMethodsDemo1;
// Define extension methods for any type that implements IMyInterface.
public static class Extension
{
public static void MethodA(this IMyInterface myInterface, int i)
{
Console.WriteLine("Extension.MethodA(this IMyInterface myInterface, int i)");
}
public static void MethodA(this IMyInterface myInterface, string s)
{
Console.WriteLine("Extension.MethodA(this IMyInterface myInterface, string s)");
}
// This method is never called, because the three classes implement MethodB.
public static void MethodB(this IMyInterface myInterface)
{
Console.WriteLine("Extension.MethodB(this IMyInterface myInterface)");
}
}
}
namespace ExtensionMethodsDemo1
{
using System;
using Extensions;
public interface IMyInterface
{
void MethodB();
}
class A : IMyInterface
{
public void MethodB(){Console.WriteLine("A.MethodB()");}
}
class B : IMyInterface
{
public void MethodB() { Console.WriteLine("B.MethodB()"); }
public void MethodA(int i) { Console.WriteLine("B.MethodA(int i)"); }
}
class C : IMyInterface
{
public void MethodB() { Console.WriteLine("C.MethodB()"); }
public void MethodA(object obj) { Console.WriteLine("C.MethodA(object obj)"); }
}
class ExtMethodDemo
{
static void Main(string[] args)
{
A a = new A();
B b = new B();
C c = new C();
TestMethodBinding(a,b,c);
}
static void TestMethodBinding(A a, B b, C c)
{
// A has no methods, so each call resolves to
// the extension methods whose signatures match.
a.MethodA(1); // Extension.MethodA(object, int)
a.MethodA("hello"); // Extension.MethodA(object, string)
a.MethodB(); // A.MethodB()
// B itself has a method with this signature.
b.MethodA(1); // B.MethodA(int)
b.MethodB(); // B.MethodB()
// B has no matching method, but Extension does.
b.MethodA("hello"); // Extension.MethodA(object, string)
// In each case C has a matching instance method.
c.MethodA(1); // C.MethodA(object)
c.MethodA("hello"); // C.MethodA(object)
c.MethodB(); // C.MethodB()
}
}
}
/* Output:
Extension.MethodA(this IMyInterface myInterface, int i)
Extension.MethodA(this IMyInterface myInterface, string s)
A.MethodB()
B.MethodA(int i)
B.MethodB()
Extension.MethodA(this IMyInterface myInterface, string s)
C.MethodA(object obj)
C.MethodA(object obj)
C.MethodB()
*/
通用准则
通常,建议您只在不得已的情况下才实现扩展方法,并谨慎地实现。只要有可能,必须扩展现有类型的客户端代码都应该通过创建从现有类型派生的新类型来达到这一目的。
在使用扩展方法来扩展您无法更改其源代码的类型时,您需要承受该类型实现中的更改会导致扩展方法失效的风险。
如果您确实为给定类型实现了扩展方法,请记住以下两点:
如果扩展方法与该类型中定义的方法具有相同的签名,则扩展方法永远不会被调用。
扩展方法被在命名空间级别放入范围中。例如,如果您在同一个名为 Extensions 的命名空间中具有多个包含扩展方法的静态类,则这些扩展方法将全部由 using Extensions; 指令放入范围中。
类库的实施者不应使用扩展方法来避免创建程序集的新版本。如果您要向库中添加重要的新功能,并且您拥有源代码,则应该遵循标准 .NET Framework 程序集版本控制准则。