居家隔离的第26天,还在持续的疫情着实让人担忧,看着每天新增的确认人数数字,也在为那些家庭祝福,每当想想那不是一个数字是一条条鲜活的生命时就格外沉重。利用闲在家里的时间巩固C#语言的一个难点。最近在温习刘铁锰老师教学视频《C#语言入门详解》加上翻看其他的电子图书巩固自己对一些难点知识的印象,好记性不如烂笔头,组织语言记录下来效果更佳。各种方法通过不同的逻辑和顺序组合在一起就形成了程序,常规都是带有参数的方法,参数可以分为以下几类:
- 传值参数
- 引用参数
- 输出参数
- 数组参数
- 可选参数
- 具名参数
- 扩展方法(this参数)
1、传值参数:声明时不带任何修饰符的形参是值形参,一个值形参对应一个局部变量,只是它的初始值来自该方法调用所提供的相对形参。形参是实参的副本,给形参赋值并不影响实参。针对参数的数据类型为值类型和引用类型分为两种情况进行讨论。
虚线以下是方法之内,虚线以上是方法之外。值参数实际上是方法内部的一个局部变量,是我们传进来的实参的一个副本,用实线框出来表明是副本关系,不会互相影响。
右边是说在方法体内对参数进行了赋值,参数获得了新值,方法外实际参数并不会改变。
通过观察实例的运行结果可以得到一致的结论,
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 /// <summary> 8 /// 值参数也叫传值参数 9 /// 定义:声明时不带修饰符的形参是值形参。 10 /// 一个值形参对应了一个局部变量,只是它的初始值来自该方法调用所提供的相对实参。 11 /// </summary> 12 namespace BiliBiliVideoCSharpDemo.Paramter 13 { 14 /// <summary> 15 /// 数据类型为值类型的值参数 16 /// </summary> 17 public class ValueParamter 18 { 19 public void AddOne(int paramOne) 20 { 21 paramOne = paramOne + 1; 22 Console.WriteLine("paramOne ="+paramOne); 23 } 24 } 25 } 26 27 28 using System; 29 using System.Collections.Generic; 30 using System.Linq; 31 using System.Text; 32 using System.Threading.Tasks; 33 using BiliBiliVideoCSharpDemo.Paramter; 34 35 namespace BiliBiliVideoCSharpDemo 36 { 37 class Program 38 { 39 static void Main(string[] args) 40 { 41 #region 测试参数类型为值类型的传值参数示例 42 ValueParamter valueParamter = new ValueParamter(); 43 int y = 100; 44 valueParamter.AddOne(y); 45 Console.WriteLine(y); 46 #endregion
47 48 Console.ReadLine(); 49 }
50 } 51 }
输出结果为:
paramOne =101
100
表明实参y还是100并未被修改,可以结合前面对于值类型的内存分析,方法AddOne调用时会在栈中分配一个新的局部变量(形参),变量初始化时存放的是实参一样的数值,变量和实参在栈中是两个地址。
实参(引用类型变量)存储所引用对象的地址,给形参(方法内参数,实参的副本)赋初始值时是把存储的地址赋值给形参。
右边我们在方法内部给形参赋新值,一般情况下为引用变量赋新值时赋值号的右边一般是new操作符的表达式。new操作符的作用就是根据数据类型创建对象,并且调用对象的实例构造器,然后再把实例在堆内存中的地址通过赋值符号交给我们的引用变量。结果就是实参和形参存储的是两个不一样的地址。
还是结合实例来观察结果是不是的确如此:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using BiliBiliVideoCSharpDemo.Paramter; namespace BiliBiliVideoCSharpDemo { class Program { static void Main(string[] args) { #region 测试参数类型为引用类型的传值参数示例 方法中给引用类型对象赋新值 //方法内部给方法参数赋了新值(给对象赋新值一般就是使用new操作符),对方法参数的修改并不会影响传入进来的实参。 //根据输出的Hashcode就可以发现前后是两个对象。 Student tim = new Student(); tim.Name = "Tim"; NewObject(tim); PrintStudent(tim); #endregion Console.ReadLine(); } public static void NewObject(Student student) { student = new Student { Name = "Tom" }; PrintStudent(student); }/// <summary> /// 打印对象信息 /// </summary> /// <param name="stu"></param> public static void PrintStudent(Student stu) { Console.WriteLine(string.Format("Hascode:{0},Name:{1}", stu.GetHashCode(), stu.Name)); } } public class Student { public string Name { get; set; } } }
输出结果:HashCode值跟引用对象在堆上的内存地址相关,所以运行结果不一定和我的一致。
Hascode:46104728,Name:Tom
Hascode:12289376,Name:Tim
根据hasCode可以判断形参和实参不是同一个对象,也符合之前对于传值参数的定义。有同学可能疑问,如果不采用new操作重新给形参赋值,只是修改形参的属性值呢?结果是形参和实参的引用是同一个对象,属性值也一样,但是方法调用时栈中会创建一个实参的副本,里面存储的值和实参存储的值一样,实参存储的值是引用对象在堆上的内存地址,所以形参存储的也是引用对象在内存中的地址,修改对象属性时先通过引用对象的地址找到引用对象再修改对象的属性。作为一个方法而言,它的主要输出还是靠它的返回值,一般情况下我们把这种修改参数所引用的对象的值的操作叫做某个方法的副作用,不是方法的主作用,是方法顺带的作用,一般不会这样书写代码,并且C#为我们这种显式利用方法副作用的需求设计了引用参数。
2、引用参数::引用形参是用ref修饰符声明的形参。引用形参并不创建新的存储位置。相反,引用形参表示的存储位置恰好是在方法调用中作为实参给出的那个变量所表示的存储位置。
注意:当形参为引用形参时,方法调用中的对应实参必须由关键字ref并后接一个与形参类型相同的variablereference组成。变量在可以作为引用形参传递之前,必须先明确赋值。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace BiliBiliVideoCSharpDemo { class Program { static void Main(string[] args) { #region 测试参数类型为引用类型的引用参数示例 Student stu = new Student() { Name = "RefObject" }; RefObject(ref stu); PrintStudent(stu); Student stu2 = new Student() { Name = "Object" }; Object(stu2); PrintStudent(stu2); //对比参数类型为引用类型的引用参数和参数类型为引用类型的传值参数,发现观察输出效果是一样的, //但是实际上的机理不一样,引用参数并不会创建实参的副本,并不会创建新的变量存储在栈上,参数类型为引用类型的传值参数会创建指向实参地址的新变量 #endregion Console.ReadLine(); } public static void Object(Student stu) { stu.Name = "ObjectTwo"; PrintStudent(stu); } public static void RefObject(ref Student stu) { stu.Name = "RefObjectTwo"; PrintStudent(stu); } /// <summary> /// 打印对象信息 /// </summary> /// <param name="stu"></param> public static void PrintStudent(Student stu) { Console.WriteLine(string.Format("Hascode:{0},Name:{1}", stu.GetHashCode(), stu.Name)); } } public class Student { public string Name { get; set; } } }
输出结果:
Hascode:43495525,Name:RefObjectTwo
Hascode:43495525,Name:RefObjectTwo
Hascode:55915408,Name:ObjectTwo
Hascode:55915408,Name:ObjectTwo
可以发现方法内的局部变量(形参)跟方法外的实参是相等的,在源码的注释中标注了参数数据类型为引用类型的传值参数和引用参数的差异。
3、输出参数:用out修饰符声明的形参是输出形参。类似于引用形参,输出形参不创建新的存储位置。相反,输出形参表示的存储位置恰是在该方法调用中作为实参给出的那个变量所表示的存储位置。输出变量用于除返回值外还需要输出的场景。
注意:1)当形参为输出形参时,方法调用中的相应实参必须由关键字out并后接-一个与形参类型相同的变量组成。变量在可以作为输出形参传递之前不一定需要明确赋值,但是在将变量作为输出形参传递的调用之后,该变量被认为是明确赋值的。
2)在方法内部,与局部变量相同,输出形参最初被认为是未赋值的,因而必须在使用它的值之前明确赋值。在方法返回之前,该方法的每个输出形参都必须明确赋值。
这里给出一个案例:
/// <summary> /// 将输入内容转换为整型,转换失败返回false /// </summary> /// <param name="input">输入字符串内容</param> /// <param name="result"返回值></param> /// <returns>false-失败 true-成功</returns> static bool ConvertToInt(string input,out int result) { try { result = Convert.ToInt32(input); return true; } catch (Exception) { result = 0;//这里必须添加输出参数的赋值语句,输出参数必须要在方法返回之前进行赋值操作,输出参数的应用场景是用于除返回值外还需要输出的情况。 return false; } } #region 测试输出参数示例 这里书写一个将输入内容转换为整型的方法,转换成功返回true否则返回false //调用必须显式加上out修饰符 ConvertToInt("123", out int result); #endregion
4、数组参数:用params修饰符声明的形参是数组参数,注意:数组参数必须是形参列表的最后一个,调用时跟ref out修饰的不太一样,并不需要使用params,也不允许这样书写。直接看入参为数组类型参数的传值参数和数组参数的区别吧。你可能还会发现很多内置方法中使用了数组参数,例如String.Format方法就有一个object类型的数组参数。
/// <summary> /// 使用数组参数简化数组类型入参的方法的调用方式 /// 方法声明时需要使用params修饰符修饰,并且必须是方法形参列表的最后一个参数 /// </summary> /// <param name="arr"></param> /// <returns></returns> static int GetSumByParams(params int[] arr) { int result = 0; foreach (int item in arr) { result += item; } return result; } /// <summary> /// 求长整型数组的和 /// </summary> /// <param name="arr"></param> /// <returns></returns> static int GetSum(int[] arr) { int result = 0; foreach (int item in arr) { result += item; } return result; } #region 对比方法入参为数组使用数组参数和传统传值参数调用方式的区别 //传统传值参数需要先构建数组 int[] arr = new int[] { 1,2,3,4,5}; Console.WriteLine(GetSum(arr)); Console.WriteLine("-----------"); //不需要先构建数组但是也可以使用数组 Console.WriteLine(GetSumByParams(1,2,3,4,5)); Console.WriteLine(GetSumByParams(arr)); Console.WriteLine(String.Format("{0}+{1}={2}",1,2,3)); Console.WriteLine(String.Format("{0}+{1}={2}", new object[] { 1, 2, 3 })); #endregion
5、具名参数:这实际上是一种方法调用方式的调整,方法声明跟传统传值参数并没有区别方法调用时以方法形参名+ : +参数数值传进方法的形式调用方法,好处就是参数的位置不受约束不必跟方法声明时参数顺序一致,还有就是方法调用时可以较容易分辨参数的含义是什么。接下来是案例时间:
/// <summary> /// 跟别人打招呼 /// </summary> /// <param name="myName">我的名字</param> /// <param name="myAge">我的年龄</param> static void GreetToPeople(string myName, int myAge,string peopleName) { Console.WriteLine(string.Format("Hello,{2}.my name is {0},I am {1} years old.",myName,myAge,peopleName)); } #region 具名参数的使用 //参数的位置不再约束 GreetToPeople(peopleName:"Bob", myAge: 26, myName: "Tom"); //允许只对部分形参使用具名的形式,但是必须放在所有非具名参数的后面 当然此时非具名参数必须跟方法形参对应 GreetToPeople("Powter", peopleName: "Bob", myAge: 26); #endregion
6、可选参数:参数因为具有默认值而变得“可选”即可以不传入,要注意可选参数必须在方法的参数列表中非可选参数的后面。直接看案例:
static void SayHello(string Name = "Tom", int Age = 18) { Console.WriteLine("Hello,my name is"+Name+",i am "+Age+"years old."); } #region 可选参数的使用 SayHello(); SayHello("Powter"); //SayHello(26); 这种写法是不允许的,因为会认为传进来的参数是第一个可选参数的数值,结果就出现类型不匹配,所以表明有多个可选参数调用时,给可选参数赋值时前面的可选参数必须赋值 //可是使用具名参数解决上一个问题 SayHello(Age: 26); #endregion
7、扩展方法(this参数):由this修饰的参数,必须是方法参数列表中的第一个参数,并且方法必须是公共的、静态的,即被public static所修饰,必须包含在一个静态类中。扩展方法一般是对某个类(例如叫Double)进行扩展,所以建议静态类命名为 DoubleExtension 。
例如我们想对一个double类型的数值进行按精度舍入时,我们一般都是借用Math.Round(double value,int digits)方法实现,可是每次都这样就比较麻烦,要是能直接x.Round(int digits)就好了,使用扩展方法就可以实现功能。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace BiliBiliVideoCSharpDemo { public static class DoubleExtension { public static double Round(this double value, int digits) { double result = 0; result = Math.Round(value, digits); return result; } } } #region this参数的使用 double pi = 3.1415926; Console.WriteLine(pi.Round(3)); #endregion
输入pi.编辑器智能提醒就可以看到多了一个Round方法,前面的图标跟正常方法不太一样,这就是扩展方法的图标。
这篇关于C#方法参数的博客到此就算结束了,主要给自己牢记这些概念。分享出来希望对读者有些帮助,有什么不妥之处欢迎留言讨论。