这一节主要讲参数,参数是方法的一部分,所以这节课也可以看作是对方法的进一步学习。

本节内容:

  • 传值参数
  • 输出参数
  • 引用参数
  • 数组参数
  • 具名参数
  • 可选参数
  • 扩展方法(this 参数)

值参数

018 传值、输出、引用、数组、具名、可选参数、扩展方法 - 图1

参见 C# 图解教程 第五章 方法。

传值参数 -> 值类型

018 传值、输出、引用、数组、具名、可选参数、扩展方法 - 图2

传值参数 -> 引用类型,并且新创建对象

018 传值、输出、引用、数组、具名、可选参数、扩展方法 - 图3

这种状况很少见,一般情况都是传进来引用它的值,而不是连接到新对象去(基本只有面试题会考这个)。

注:当参数类型为 string 时,在方法内部修改参数的值,对应的是此处创建对象这种情况。 因为 string 是 immutable 的,所以在方法内部对 string 赋值实际是“创建新的 string 实例再赋值”,最终方法外部的 string 并不会改变。

GetHashCode()

Object.GetHashCode() 方法,用于获取当前对象的哈希代码,每个对象的 Hash Code 都不一样。

通过 Hash Code 来区分两个 Name 相同的 stu 对象。

  1. class Program
  2. {
  3. static void Main(string[] args)
  4. {
  5. var stu = new Student() { Name="Tim"};
  6. SomeMethod(stu);
  7. Console.WriteLine(stu.Name);
  8. Console.WriteLine(stu.GetHashCode());
  9. }
  10. static void SomeMethod(Student stu)
  11. {
  12. stu = new Student { Name = "Tim" };
  13. Console.WriteLine(stu.Name);
  14. Console.WriteLine(stu.GetHashCode());
  15. }
  16. }
  17. class Student
  18. {
  19. public string Name { get; set; }
  20. }

传值参数 -> 引用类型,只操作对象,不创建新对象

018 传值、输出、引用、数组、具名、可选参数、扩展方法 - 图4

这种通过传递进来的参数修改其引用对象的值的情况,在工作中也比较少见。

因为作为方法,其主要输出还是靠返回值。我们把这种修改参数所引用对象的值的操作叫做方法的副作用(side-effect),这种副作用平时编程时要尽量避免。

引用参数 ref

018 传值、输出、引用、数组、具名、可选参数、扩展方法 - 图5

引用参数 -> 值类型

018 传值、输出、引用、数组、具名、可选参数、扩展方法 - 图6

  1. static void Main(string[] args)
  2. {
  3. int y = 1;
  4. IWantSideEffect(ref y);
  5. Console.WriteLine(y);
  6. }
  7. static void IWantSideEffect(ref int x)
  8. {
  9. x += 100;
  10. }

引用参数 -> 引用类型,创建新对象

018 传值、输出、引用、数组、具名、可选参数、扩展方法 - 图7

  1. class Program
  2. {
  3. static void Main(string[] args)
  4. {
  5. var outterStu = new Student() { Name = "Tim" };
  6. Console.WriteLine("HashCode={0}, Name={1}", outterStu.GetHashCode(), outterStu.Name);
  7. Console.WriteLine("-----------------");
  8. IWantSideEffect(ref outterStu);
  9. Console.WriteLine("HashCode={0}, Name={1}", outterStu.GetHashCode(), outterStu.Name);
  10. }
  11. static void IWantSideEffect(ref Student stu)
  12. {
  13. stu = new Student() { Name = "Tom" };
  14. Console.WriteLine("HashCode={0}, Name={1}",stu.GetHashCode(),stu.Name);
  15. }
  16. }
  17. class Student
  18. {
  19. public string Name { get; set; }
  20. }

018 传值、输出、引用、数组、具名、可选参数、扩展方法 - 图8

引用参数 -> 引用类型,不创建新对象只改变对象值

018 传值、输出、引用、数组、具名、可选参数、扩展方法 - 图9

对象的 HashCode 没有改变过。

  1. class Program
  2. {
  3. static void Main(string[] args)
  4. {
  5. var outterStu = new Student() { Name = "Tim" };
  6. Console.WriteLine("HashCode={0}, Name={1}", outterStu.GetHashCode(), outterStu.Name);
  7. Console.WriteLine("-----------------");
  8. SomeSideEffect(ref outterStu);
  9. Console.WriteLine("HashCode={0}, Name={1}", outterStu.GetHashCode(), outterStu.Name);
  10. }
  11. static void SomeSideEffect(ref Student stu)
  12. {
  13. stu.Name = "Tom";
  14. Console.WriteLine("HashCode={0}, Name={1}", stu.GetHashCode(), stu.Name);
  15. }
  16. }
  17. class Student
  18. {
  19. public string Name { get; set; }
  20. }

018 传值、输出、引用、数组、具名、可选参数、扩展方法 - 图10

注:上面示例中使用传值参数(不用 ref)结果也将一样,但内部机理不同。
传值参数创建了副本,方法里面的 stu 和 outterStu 不是一个对象,所指向的内存地址不一样,但是存储的地址是相同的,都存储的是 Student 实例在堆内存中的地址。

引用参数 stu 和 outterStu 指向的是同一个内存地址,这个内存地址里面存储的就是 Student 实例在堆内存中的地址。

输出形参 out

018 传值、输出、引用、数组、具名、可选参数、扩展方法 - 图11

输出参数 -> 值类型

018 传值、输出、引用、数组、具名、可选参数、扩展方法 - 图12

  1. static void Main(string[] args)
  2. {
  3. Console.WriteLine("Please input first number:");
  4. var arg1 = Console.ReadLine();
  5. double x = 0;
  6. if (double.TryParse(arg1, out x) == false)
  7. {
  8. Console.WriteLine("Input error!");
  9. return;
  10. }
  11. Console.WriteLine("Please input second number:");
  12. var arg2 = Console.ReadLine();
  13. double y = 0;
  14. if (double.TryParse(arg2, out y) == false)
  15. {
  16. Console.WriteLine("Input error!");
  17. return;
  18. }
  19. double z = x + y;
  20. Console.WriteLine(z);
  21. }

自己实现了带有输出参数的 TryParse:

  1. class Program
  2. {
  3. static void Main(string[] args)
  4. {
  5. double x = 0;
  6. if(DoubleParser.TryParse("aa",out x))
  7. {
  8. Console.WriteLine(x);
  9. }
  10. }
  11. }
  12. class DoubleParser
  13. {
  14. public static bool TryParse(string input,out double result)
  15. {
  16. try
  17. {
  18. result = double.Parse(input);
  19. return true;
  20. }
  21. catch
  22. {
  23. result = 0;
  24. return false;
  25. }
  26. }
  27. }

输出参数 -> 引用类型

018 传值、输出、引用、数组、具名、可选参数、扩展方法 - 图13

引用类型的输出参数实例

  1. class Program
  2. {
  3. static void Main(string[] args)
  4. {
  5. Student stu = null;
  6. if(StudentFactory.Create("Tim", 34, out stu))
  7. {
  8. Console.WriteLine("Student {0}, age is {1}",stu.Name,stu.Age);
  9. }
  10. }
  11. }
  12. class Student
  13. {
  14. public int Age { get; set; }
  15. public string Name { get; set; }
  16. }
  17. class StudentFactory
  18. {
  19. public static bool Create(string stuName,int stuAge,out Student result)
  20. {
  21. result = null;
  22. if (string.IsNullOrEmpty(stuName))
  23. {
  24. return false;
  25. }
  26. if (stuAge < 20 || stuAge > 80)
  27. {
  28. return false;
  29. }
  30. result = new Student() { Name = stuName, Age = stuAge };
  31. return true;
  32. }
  33. }

数组参数 params

  • 必需是形参列表中的最后一个,由 params 修饰
  • 举列:String.Format 方法和 String.Split 方法

使用 params 关键字前:

  1. class Program
  2. {
  3. static void Main(string[] args)
  4. {
  5. var myIntArray = new int[] { 1, 2, 3 };
  6. int result = CalculateSum(myIntArray);
  7. Console.WriteLine(result);
  8. }
  9. static int CalculateSum(int[] intArray)
  10. {
  11. int sum = 0;
  12. foreach (var item in intArray)
  13. {
  14. sum += item;
  15. }
  16. return sum;
  17. }
  18. }

使用 params 后,不再需要单独声明数组:

  1. class Program
  2. {
  3. static void Main(string[] args)
  4. {
  5. int result = CalculateSum(1, 2, 3);
  6. Console.WriteLine(result);
  7. }
  8. static int CalculateSum(params int[] intArray)
  9. {
  10. int sum = 0;
  11. foreach (var item in intArray)
  12. {
  13. sum += item;
  14. }
  15. return sum;
  16. }
  17. }

我们早在 WriteLine 方法中就用到了 params。
018 传值、输出、引用、数组、具名、可选参数、扩展方法 - 图14

又一个用到了数组参数(params)的例子。
018 传值、输出、引用、数组、具名、可选参数、扩展方法 - 图15

具名参数

具名参数:参数的位置不再受约束。

具名参数的优点:

  • 提高代码可读性

  • 参数的位置不在受参数列表约束

具名参数实例:

  1. class Program
  2. {
  3. static void Main(string[] args)
  4. {
  5. PrintInfo("Tim", 34);
  6. PrintInfo(age: 24, name:"Wonder");
  7. }
  8. static void PrintInfo(string name, int age)
  9. {
  10. Console.WriteLine("Helllo {0}, you are {1}.",name,age);
  11. }
  12. }

可选参数

  • 参数因为具有默认值而变得“可选”

  • 不推荐使用可选参数

扩展方法

018 传值、输出、引用、数组、具名、可选参数、扩展方法 - 图16

无扩展方法:

  1. class Program
  2. {
  3. static void Main(string[] args)
  4. {
  5. double x = 3.14159;
  6. // double 类型本身没有 Round 方法,只能使用 Math.Round。
  7. double y = Math.Round(x, 4);
  8. Console.WriteLine(y);
  9. }
  10. }

有扩展方法后:

  1. class Program
  2. {
  3. static void Main(string[] args)
  4. {
  5. double x = 3.14159;
  6. // double 类型本身没有 Round 方法,只能使用 Math.Round。
  7. double y = x.Round(4);
  8. Console.WriteLine(y);
  9. }
  10. }
  11. static class DoubleExtension
  12. {
  13. public static double Round(this double input,int digits)
  14. {
  15. return Math.Round(input, digits);
  16. }
  17. }

018 传值、输出、引用、数组、具名、可选参数、扩展方法 - 图17

当我们无法修改类型源码时,可以通过扩展方法为目标数据类型追加方法。
LINQ 也是扩展方法的一大体现。

LINQ 实例

  1. class Program
  2. {
  3. static void Main(string[] args)
  4. {
  5. var myList = new List<int>(){ 11, 12, 9, 14, 15 };
  6. //bool result = AllGreaterThanTen(myList);
  7. // 这里的 All 就是一个扩展方法
  8. bool result = myList.All(i => i > 10);
  9. Console.WriteLine(result);
  10. }
  11. static bool AllGreaterThanTen(List<int> intList)
  12. {
  13. foreach (var item in intList)
  14. {
  15. if (item <= 10)
  16. {
  17. return false;
  18. }
  19. }
  20. return true;
  21. }
  22. }

All 第一个参数带 this,确实是扩展方法。
018 传值、输出、引用、数组、具名、可选参数、扩展方法 - 图18

总结

各种参数的使用场景总结:

  • 传值参数:参数的默认传递方法
  • 输出参数:用于除返回值外还需要输出的场景
  • 引用参数:用于需要修改实际参数值的场景
  • 数组参数:用于简化方法的调用
  • 具名参数:提高可读性
  • 可选参数:参数拥有默认值
  • 扩展方法(this 参数):为目标数据类型“追加”方法