值参数
声明时不带修饰符的形参是值形参。一个值形参对应于一个局部变量,只是它的初始值来自该方法调用所提供的相应实参。
当形参是值形参时,方法调用中的对应实参必须是表达式,并且它的类型可以隐式转换为形参的类型。
允许方法将新值赋给值参数。这样的赋值只影响由该值形参表示的局部存储位置,而不会影响在方法调用时由调用方给出的实参。
传值参数 -> 值类型
注意
- 值参数创建变量的副本
- 对值参数的操作永远不影响变量的值
class Program
{
static void Main(string[] args)
{
Student stu = new Student();
int y = 100;
stu.AddOne(y);
Console.WriteLine(y);//y的值并没有改变
}
}
class Student
{
public void AddOne(int x)//int为参数的数据类型,并不是修饰符
{
x = x + 1;
Console.WriteLine(x);
}
}
传值参数 -> 引用类型,并且新创建对象
注意
- 值参数创建变量的副本
- 对值参数的操作永远不影响变量的值
这种状况很少见,一般情况都是传进来引用它的值,而不是连接到新对象去(基本只有面试题会考这个)。
注:当参数类型为 string 时,在方法内部修改参数的值,对应的是此处创建对象这种情况。 因为 string 是 immutable 的,所以在方法内部对 string 赋值实际是“创建新的 string 实例再赋值”,最终方法外部的 string 并不会改变。
class Program
{
static void Main(string[] args)
{
Student stu = new Student() { Name="Tim"};
SomeMethod(stu);
Console.WriteLine(stu.Name);
}
static void SomeMethod(Student stu)
{
stu = new Student() {Name="Tom" };
Console.WriteLine(stu.Name);
}
}
class Student
{
public string Name { get; set; }
}
GetHashCode()
Object.GetHashCode()
方法,用于获取当前对象的哈希代码,每个对象的 Hash Code 都不一样。
通过 Hash Code 来区分两个 Name 相同的 stu 对象。
class Program
{
static void Main(string[] args)
{
var stu = new Student() { Name="Tim"};
SomeMethod(stu);
Console.Write(stu.Name);
Console.WriteLine(stu.GetHashCode());
}
static void SomeMethod(Student stu)
{
stu = new Student { Name = "Tim" };
Console.Write(stu.Name);
Console.WriteLine(stu.GetHashCode());
}
}
class Student
{
public string Name { get; set; }
}
传值参数 -> 引用类型,只操作对象,不创建新对象
注意
- 对象还是那个对象,但对象里的值(字段/属性)已经改变
class Program
{
static void Main(string[] args)
{
Student stu = new Student() { Name="Tim"};
UpdateObject(stu);
Console.WriteLine("HashCode={0},Name={1}", stu.GetHashCode(), stu.Name);
}
static void UpdateObject(Student stu)
{
stu.Name = "Tom";
Console.WriteLine("HashCode={0},Name={1}",stu.GetHashCode(),stu.Name);
}
}
class Student
{
public string Name { get; set; }
}
这种通过传递进来的参数修改其引用对象的值的情况,在工作中也比较少见。
因为作为方法,其主要输出还是靠返回值。我们把这种修改参数所引用对象的值的操作叫做方法的副作用(side-effect),这种副作用平时编程时要尽量避免。
引用参数 ref
:::info
引用形参是用ref修饰符声明的形参。与值形参不同,引用形参并不创建新的存储位置。相反,引用形参表示的存储位置恰是在方法调用中作为实参给出的那个变量所表示的存储位置。
当形参为引用形参时, 方法调用中的对应实参必须由关键字ref并后接一个与形参类型相同的variable-reference组成。变量在可以作为引用形参传递之前, 必须先明确赋值。
在方法内部,引用形参始终被认为是明确赋值的。
声明为迭代器的方法不能有引用形参。
:::
:::danger
引用参数可以直接认为对实参进行操作
:::
引用参数 -> 值类型
static void Main(string[] args)
{
int y = 1;
IWantSideEffect(ref y);
Console.WriteLine(y);
}
static void IWantSideEffect(ref int x)
{
x += 100;
}
引用参数 -> 引用类型,创建新对象
注意
- 引用参数并不创建变量的副本
- 使用ref修饰符显式指出-——此方法的副作用是改变实际参数的值
class Program
{
static void Main(string[] args)
{
var outterStu = new Student() { Name = "Tim" };
Console.WriteLine("HashCode={0}, Name={1}", outterStu.GetHashCode(), outterStu.Name);
Console.WriteLine("-----------------");
IWantSideEffect(ref outterStu);
Console.WriteLine("HashCode={0}, Name={1}", outterStu.GetHashCode(), outterStu.Name);
}
static void IWantSideEffect(ref Student stu)
{
stu = new Student() { Name = "Tom" };
Console.WriteLine("HashCode={0}, Name={1}",stu.GetHashCode(),stu.Name);
}
}
class Student
{
public string Name { get; set; }
}
引用参数 -> 引用类型,不创建新对象只改变对象值
对象的 HashCode 没有改变过。
class Program
{
static void Main(string[] args)
{
var outterStu = new Student() { Name = "Tim" };
Console.WriteLine("HashCode={0}, Name={1}", outterStu.GetHashCode(), outterStu.Name);
Console.WriteLine("-----------------");
SomeSideEffect(ref outterStu);
Console.WriteLine("HashCode={0}, Name={1}", outterStu.GetHashCode(), outterStu.Name);
}
static void SomeSideEffect(ref Student stu)
{
stu.Name = "Tom";
Console.WriteLine("HashCode={0}, Name={1}", stu.GetHashCode(), stu.Name);
}
}
class Student
{
public string Name { get; set; }
}
注:上面示例中使用传值参数(不用 ref)结果也将一样,但内部机理不同。
传值参数创建了副本,方法里面的 stu 和 outterStu 不是一个对象,所指向的内存地址不一样,但是存储的地址是相同的,都存储的是Student实例在堆内存中的地址。
引用参数stu和outterStu指向的是同一个内存地址,这个内存地址里面存储的就是Student实例在堆内存中的地址。
输出形参 out
:::info
用out修饰符声明的形参是输出形参。类似于引用形参,输出形参不创建新的存储位置。相反,输出形参表示的存储位置恰是在该方法调用中作为实参给出的那个变量所表示的存储位置。
当形参为输出形参时,方法调用中的相应实参必须由关键字out并后接一个与形参类型相同的variable-reference组成。变量在可以作为输出形参传递之前不一定需要明确赋值,但是在将变量作为输出形参传递的调用之后,该变量被认为是明确赋值的。
在方法内部,与局部变量相同,输出形参最初被认为是未赋值的,因而必须在使用它的值之前明确赋值。
在方法返回之前,该方法的每个输出形参都必须明确赋值。
声明为分部方法或迭代器的方法不能有输出形参。
输出形参通常用在需要产生多个返回值的方法中。
:::
输出参数->值类型
注意
- 输出参数并不创建变量的副本
- 方法体内必需要有对输出变量的赋值的操作
- 使用out修饰符显式指出——此方法的副作用是通过参数向外输出值
- 从语义上来讲ref是为了“改变", out是为了“输出”
static void Main(string[] args)
{
Console.WriteLine("Please input first number:");
var arg1 = Console.ReadLine();
double x = 0;
if (double.TryParse(arg1, out x) == false)
{
Console.WriteLine("Input error!");
return;
}
Console.WriteLine("Please input second number:");
var arg2 = Console.ReadLine();
double y = 0;
if (double.TryParse(arg2, out y) == false)
{
Console.WriteLine("Input error!");
return;
}
double z = x + y;
Console.WriteLine(z);
}
自己实现了带有输出参数的 TryParse:
class Program
{
static void Main(string[] args)
{
double x = 0;
if(DoubleParser.TryParse("aa",out x))
{
Console.WriteLine(x);
}
}
}
class DoubleParser
{
public static bool TryParse(string input,out double result)
{
try
{
result = double.Parse(input);
return true;
}
catch
{
result = 0;
return false;
}
}
}
输出参数 -> 引用类型
注意
- 输出参数并不创建变量的副本
- 方法体内必需要有对输出变量的赋值的操作
- 使用out修饰符显式指出——此方法的副作用是通过参数向外输出值
- 从语义上来讲ref是为了“改变", out是为了“输出”
class Program
{
static void Main(string[] args)
{
Student stu = null;
if(StudentFactory.Create("Tim", 34, out stu))
{
Console.WriteLine("Student {0}, age is {1}",stu.Name,stu.Age);
}
}
}
class Student
{
public int Age { get; set; }
public string Name { get; set; }
}
class StudentFactory
{
public static bool Create(string stuName,int stuAge,out Student result)
{
result = null;
if (string.IsNullOrEmpty(stuName))
{
return false;
}
if (stuAge < 20 || stuAge > 80)
{
return false;
}
result = new Student() { Name = stuName, Age = stuAge };
return true;
}
}
数组参数 params
- 必需是形参列表中的最后一个,由 params 修饰
- 举列:String.Format 方法和 String.Split 方法
使用 params 关键字前:
class Program
{
static void Main(string[] args)
{
var myIntArray = new int[] { 1, 2, 3 };
int result = CalculateSum(myIntArray);
Console.WriteLine(result);
}
static int CalculateSum(int[] intArray)
{
int sum = 0;
foreach (var item in intArray)
{
sum += item;
}
return sum;
}
}
使用 params 后,不再需要单独声明数组:
class Program
{
static void Main(string[] args)
{
int result = CalculateSum(1, 2, 3);
Console.WriteLine(result);
}
static int CalculateSum(params int[] intArray)
{
int sum = 0;
foreach (var item in intArray)
{
sum += item;
}
return sum;
}
}
我们早在 WriteLine 方法中就用到了 params。
又一个用到了数组参数(params)的例子。
具名参数
具名参数:参数的位置不再受约束。
具名参数的优点:
- 提高代码可读性
- 参数的位置不在受参数列表约束
具名参数实例:
class Program
{
static void Main(string[] args)
{
PrintInfo("Tim", 34);
PrintInfo(age: 24, name:"Wonder");
}
static void PrintInfo(string name, int age)
{
Console.WriteLine("Helllo {0}, you are {1}.",name,age);
}
}
可选参数
- 参数因为具有默认值而变得“可选”,带有默认值
不推荐使用可选参数
class Program
{
static void Main(string[] args)
{
PrintInfo(); //不书写数值也可运行,且填入结果为默认输入值Tim,34
}
static void PrintInfo(string name="Tim", int age=34)
{
Console.WriteLine("Helllo {0}, you are {1}.", name, age);
}
}
扩展方法(this参数)
:::info 方法必需是公有、静态的, 即被public static所修饰
必需是形参列表中的第一个, 由this修饰
必需由一个静态类(一般类名为Some Type Extension) 来统一收纳对Some Type类型的扩展方法
举例:LINQ方法 :::
无扩展方法:
class Program
{
static void Main(string[] args)
{
double x = 3.14159;
// double 类型本身没有 Round 方法,只能使用 Math.Round。
double y = Math.Round(x, 4);
Console.WriteLine(y);
}
}
有扩展方法后:
class Program
{
static void Main(string[] args)
{
double x = 3.14159;
// double 类型本身没有 Round 方法,只能使用 Math.Round。
double y = x.Round(4);
Console.WriteLine(y);
}
}
static class DoubleExtension
{
public static double Round(this double input,int digits)
{
return Math.Round(input, digits);
}
}
当我们无法修改类型源码时,可以通过扩展方法为目标数据类型追加方法。
LINQ 也是扩展方法的一大体现。
class Program
{
static void Main(string[] args)
{
var myList = new List<int>(){ 11, 12, 9, 14, 15 };
//bool result = AllGreaterThanTen(myList);
// 这里的 All 就是一个扩展方法
bool result = myList.All(i => i > 10);
Console.WriteLine(result);
}
static bool AllGreaterThanTen(List<int> intList)
{
foreach (var item in intList)
{
if (item <= 10)
{
return false;
}
}
return true;
}
}
总结
各种参数的使用场景总结:
- 传值参数:参数的默认传递方法
- 输出参数:用于除返回值外还需要输出的场景
- 引用参数:用于需要修改实际参数值的场景
- 数组参数:用于简化方法的调用
- 具名参数:提高可读性
- 可选参数:参数拥有默认值
- 扩展方法(this 参数):为目标数据类型“追加”方法