方法(method)是编程中重要的一个元素,也是一系列任务执行的关键所在,本课,将讨论在C#中如何定义方法,包括方法的返回值和不同参数类型的应用;此外,还将讨论一些方法的应用特点,主要包括:

  • 参数与返回值
  • 重载方法
  • 构造函数与方法链
  • 析构函数

首先看一下方法定义的一般形式:

[<访问修饰符>] <返回类型> <方法名> ([<参数列表>])
{
    // 方法体
}

这里:

  • <访问修饰符>可以定义方法的访问级别,包含常用的private、public、protected、internal,以及定义静态方法的static关键字、抽象方法的abstract关键字、虚拟方法的virtual关键字和重写方法的override关键字。如果没有访问修饰符,则默认为私有的访问级别。

  • <返回值类型>是指方法执行后返回的数据类型,如前面使用过的int、string等类型,也可以是后续课程所介绍的各种类型,方法体中可以使用return语句返回执行结果数据。如果方法不需要返回数据,则使用void关键字设置。

  • <方法名>当然是指方法的名称,如前面使用过的Drive()、Return()、ShowLocalModel()方法等。

  • <参数列表>用于带入方法所需要的数据,如果没有就空着。参数可以有多个,每个参数最少需要定义数据类型和参数名称,多个参数使用英文半角逗号分隔;稍后,我们会详细讨论参数的应用问题。

  • 方法体是方法真正实现功能的地方,使用一对花括号定义,这也是语句块的定义方式。

在后续的学习中会逐步了解各种软件功能的实现,而接下来,我们先讨论几种参数的设置和应用。

参数与返回值

普通参数

普通参数只需要指定数据类型和参数变量,如下面的代码,我们在ConsoleTest项目中创建一个名为C5的类,其中定义了一个Add()静态方法。

namespace ConsoleTest
{
    public class C5
    {
        public static int Add(int x, int y)
        {
            return x + y;
        }
    }
}

方法功能很简单,只是用来计算两个32位整数(int类型)的和(很明显是把事情搞复杂了!^_^);下面的代码,我们在Program.cs文件中测试C5.Add()方法。

using System;

namespace ConsoleTest
{
    class Program
    {
        static void Main(string[] args)
        {
            int x = 10;
            int y = 99;
            Console.WriteLine("{0} + {1} = {2}", x, y, C5.Add(x, y));
        }
    }
}

代码执行结果如下图所示。

enter image description here

现在,注意区分Add()方法中的x和y参数,以及Main()方法中的x和y变量,在前面的课程中,我们提到过数据传递过程中的访问级别问题。在Add()的参数中定义的x和y只能在Add()方法体中使用;Main()方法中定义的x和y变量,在调用Add()方法时,会将变量的数据传递到Add()方法中,然后将计算结果通过return语句返回,整个过程中并没有产生歧义,所以,代码可以正常执行。

参数默认值

使用方法时,有此参数可能会有一些常用值,此时,可以将参数的数据设置默认值,以方便方法的调用。需要注意的是,有默认值的参数应放在所有没有默认值的参数的后面,如下面的代码,我们在C5类中添加了一个名为Add1()的参数。

public static int Add1(int x, int y = 0)
{
    return x + y;
}

参数中,y设置了默认值0,也就是说,在调用Add1()方法时,如果不指定参数y的数据,那么,它的数据就是0,下面,在Program.cs文件中测试此方法的使用。

using System;

namespace ConsoleTest
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(C5.Add1(10));
            Console.WriteLine(C5.Add1(10, 99));
        }
    }
}

代码执行结果如下图。

enter image description here

参数数组

前面创建的Add()和Add1()方法,可以使用一个或两个参数,如果是需要不定数量的参数应该怎么办呢?此时,可以使用参数数组,如下面的代码,我们在C5类中定义一个Add2()方法。

public static int Add2(int x, params int[] nums)
{
    int sum = x;
    for (int i = 0; i < nums.Length; i++)
           sum = sum + nums[i];
    return sum;
}

本例中,请注意第二个参数,其定义如下:

params int[] nums

这里使用params关键字定义了一个int类型的数组参数nums。数组是指一组相同类型数据的集合,定义时在类型名称后加一对方括号,如本例中的int[]。

参数数组在应用时有什么特点呢?它可以使用零或多个参数,如本例中的Add2()方法,除了第一个参数是必须的,我们还可以使用第二个或更多的参数,如下面的代码,我们在Program.cs文件测试Add2()方法的使用。

using System;

namespace ConsoleTest
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine(C5.Add2(10));
            Console.WriteLine(C5.Add2(10, 99));
            Console.WriteLine(C5.Add2(1,2,3,4,5));
        }
    }
}

代码执行结果如下图所示 。

enter image description here

参数数组应用在参数列表的最后,不应和参数默认值一起使用,那样会产生歧义。

ref参数

说起ref关键字的应用,大家首先还需要理解值类型和引用类型数据的传递问题。

C#中的数据类型分为值类型和引用类型,它们在内存中的处理方式是不同的,但在直接应用时,表面上可能不太容易看出它们的区别,所在,这里着重讨论按值传递和按引用传递区别。

在C#中,一些基本的数据类型定义为值类型,如int、long、float、double、decimal等等;值类型的默认传递方式是复制其数据,如下面的代码。

using System;

namespace ConsoleTest
{
    class Program
    {
        static void Main(string[] args)
        {
            int x = 10;
            int y = x;
            Console.WriteLine("x={0},y={1}",x, y);
            x = 99;
            Console.WriteLine("x={0},y={1}", x, y);
        }
    }
}

先看一下执行结果,如下图所示。

enter image description here

代码中,首先将x赋值为10,然后将x的值赋值给y,此时,由于int是值类型,所以是将x的值复制到y中,y和x并没有什么关联;所以,当修改x的值为99后,y的值依然是10,如上图所显示的结果一样。

int是值类型,而各种class类型则是引用类型,下面的代码,我们看一下CAuto类型的赋值操作。

static void Main(string[] args)
{
        CAuto racer = new CAuto()
        {
            DoorCount = 2
        };
        CAuto suv = racer;
        Console.WriteLine("{0},{1}", racer.DoorCount, suv.DoorCount);
        suv.DoorCount = 5;
        Console.WriteLine("{0},{1}", racer.DoorCount, suv.DoorCount);
}

代码执行结果如下图所示。

enter image description here

本例中首先创建了racer对象,然后将其直接赋值给suv对象,此时,实际上是传递了racer对象在内存中的地址,也就是说suv和racer指向的是同一数据区域;接下来,只修改suv对象的数据,但racer对象的数据也改变了,这一现象证实了,只是通过简单的赋值,suv和racer对象的确是指向同一数据区域。

使用方法时,值类型的处理方式是复制数据;如果需要在方法中真正地修改原变量的数据,就需要使用ref关键字,如下面的代码,我们在C5类中定义一个Swap()方法。

public static void Swap(ref int x, ref int y)
{
    int z = x;
   x = y;
   y = z;
}

我们可以看到,Swap()方法的功能是交换两个int类型变量的值,只是参数定义进使用了ref关键字,下面的代码,我们在Program.cs文件中测试这个方法。

static void Main(string[] args)
{
    int x = 10;
    int y = 99;
    Console.WriteLine("x={0},y={1}",x, y);
    C5.Swap(ref x, ref y);
    Console.WriteLine("x={0},y={1}", x, y);
}

代码执行结果如下图所示。

enter image description here

请注意,当参数使用ref关键字时,调用方法设置参数时也需要使用ref关键字。

动手测试:大家可测试一下Swap()方法的参数没有使用ref关键字时的执行结果。

out参数与返回值

我们知道,方法可以通过return语句返回执行结果,但这只是在方法只有一个返回数据的情况下?那么,方法还能同时返回多个执行结果吗?

下面,我们先看一个简单的例子——如何将字符串(string)内容转换为int类型。

static void Main(string[] args)
{
    string s = "123";
    int num = 0;
    bool result = int.TryParse(s, out num);
    Console.WriteLine("转换结果:{0}", result);
    Console.WriteLine("转换数据:{0}", num);
}

代码中,可以修改s的内容来观察执行结果。变量num用于存放转换后的数据,如果不能正确转换,则初始值是0;变量result用于存放转换结果,成功转换为True值(true值),失败为False值(false值)。转换操作使用了int.TryParse()静态方法,其中,第一个参数是需要转换的字符串内容,参数二定义为out参数,用于保存转换后的数据;方法的返回值表示转换操作是否成功。

代码执行结果如下图所示。

enter image description here

如果将s的内容修改为abc,则执行结果如下图所示。

enter image description here

实际上,我们定义的方法也可以使用out参数,其功能就是让方法返回更多的数据,如下面的代码,我们在C5类中定义一下TryToInt()方法。

public static bool TryToInt(object o, out int result)
{
    if (o == null)
    {
        result = 0;
        return false;
    }
    else
    {
        return int.TryParse(o.ToString(), out result);
    }
}

这里,我们将数据转换方法进行了扩展,可以尝试将任何类型的数据转换为int类型,下面的代码,我们在Program.cs文件中测试此方法的使用。

static void Main(string[] args)
 {
    object obj = "abc";
     int num = 0;
    bool result = C5.TryToInt(obj, out num);
    Console.WriteLine("转换结果:{0}", result);
    Console.WriteLine("转换数据:{0}", num);
 }

大家可以修改obj对象的内容来观察执行结果,可以尝试空对象的null值、各种数值等类型的数据。

重载方法

现在,回顾一下Console.WriteLine()方法的调用,我们使用了多少种参数的组合?它实际上就是有多个版本的重载方法。

重载方法的基本含义是,定义一系列同名方法,并且可以通过参数的类型、个数等元素有效区别方法的不同版本。

在同一类型中,方法的重载在很多情况下可以使用参数默认值、参数数组等特性来代替,而重载应用的情景是,方法的参数差异较大,无法通过参数默认值和参数数组来处理。如下面的代码,我们又在C5类中添加了两个Swap()方法,用于交换long类型和decimal类型变量的数据。

//
public static void Swap(ref long x, ref long y)
{
    long z = x;
    x = y;
    y = z;
}
//
public static void Swap(ref decimal x, ref decimal y)
{
    decimal z = x;
    x = y;
    y = z;
}

我们可以看到,Swap()方法的参数类型是不同的,但有一点,方法中的代码逻辑是相同的,实际上,对于这种情况,可以考虑使用泛型来处理,在后续的课程中会有讨论;现在,我们需要明白的是,见到同名方法,而参数又有明显区别时并不需要感到惊讶,它们可能只是同名的重载方法而已。

构造函数与方法链

我们提到过,构造函数是一种特殊的方法,那么,特殊在什么地方呢?主要表现为不需要设置返回值类型,以及需要和类同名。

如果在类中没有定义构造函数,则会自动包含一个没有任何参数的构造函数;但是,如果手工添加了任何构造函数,就不会自动添加构造函数。

构造函数也可以通过参数默认值、参数数组、重载等形式创建多个版本,不过,我们还可以通过另外一种形式创建多版本的构造函数(或方法),如下面的代码,我们在CAuto类中定义了三个构造函数。

public class CAuto
{
    // 构造函数
    public CAuto(int doorCount,string model)
    {
        DoorCount = doorCount;
        Model = model;
    }
    //
    public CAuto(string model) : this(4, model) { }
    //
    public CAuto() : this("未知型号") { }
    //
}

本例中,我们首先创建了一个比较完整参数的构造函数,用于设置车门数(DoorCount)和型号(Model)。第二个构造函数包含一个参数,但其后使用this关键字调用了第一个构造函数,其含义是将车门设置为4,并设置型号。第三个构造函数使用this关键字调用了第二个构造函数,将型号设置为“未知型号”,此时,车门数量默认同样为4。这样逐个调用的构造函数就形成了方法链结构,实际上,不只是构造函数,其他方法同样可以通过这种形式简化多版本的定义。

下面的代码,我们在Program.cs文件中测试这三个构造函数的使用。

static void Main(string[] args)
{
    CAuto car1 = new CAuto(2, "Coupe");
    CAuto car2 = new CAuto("CX5");
    CAuto car3 = new CAuto();
    Console.WriteLine("{0},{1}", car1.DoorCount, car1.Model);
    Console.WriteLine("{0},{1}", car2.DoorCount, car2.Model);
    Console.WriteLine("{0},{1}", car3.DoorCount, car3.Model);
}

代码执行结果如下图所示。

enter image description here

析构函数

构造函数在创建对象时调用,而析构函数则会在删除对象时调用;在.NET Framework中已经有了很成熟的内存回收机制,当对象不再使用时,会自动释放内存,所以,需要手工编写析构函数的时候并不多,但大家应该知道有这样一个地方可以用来手工释放资源。

下面的代码,我们在CAuto类中添加一个简单的析构函数。

// 析构函数
~CAuto()
 {
    Console.WriteLine("汽车对象回收中");
 }

下面的代码,在Program.cs文件中测试析构函数。

static void Main(string[] args)
{
     CAuto car = new CAuto();
     //car = null;
 }

代码中,“car= null;”语句用于手工释放car对象,即将对象设置为null值,大家可测试一下,无论有没有这条语句,执行结果都如下图所示。

enter image description here

从本例中,我们可以看到,无论是手工释放对象,还是.NET Framework自动释放对象,都会调用析构函数。另一种自动释放资源的机制是通过using语句结构调用实现IDisposable接口的对象,后续内容中会有介绍。

CHY软件小屋原创作品!