方法
方法定义
方法可以将一组复杂的代码进行打包。
声明方法的语法是返回类型 + 方法名 + 括号 + 方法体
。
void Hello1()
{
for (int i = 0; i < 10; i++)
{
Console.WriteLine("Hello");
}
}
调用方法
方法的主要特征就是他的括号。
调用方法的语法是方法名+括号
。
Hello1();
当调用方法时,就会执行方法体内的代码。
在多个地方需要使用同一组复杂代码时,使用方法打包,会减少代码量。
相较于直接复制这些代码,打包成方法更方便后续的修改。
只需要修改方法体内部的代码,所有调用方法的地方执行的代码都会被修改。
作用域
方法体的声明也有一个大括号,所以这也是一个作用域。
在大括号里声明的东西,不能在大括号外访问。
如果Int2定义在一个if
块中也是同样的效果。
void Int1()
{
int i1 = 10;//这个变量只能在Int1内部访问
void Int2()//这个方法只能在Int1内部访问
{
int i2 = i1;//允许访问
}
}
Int2();//无法访问,超出作用域
i1 = 5;//无法访问,超出作用域
返回值
void
表示方法不会返回值。它可以改成一个具体类型,方法在调用完毕后就会返回这个类型的值。
必须在方法内部使用return
指明要返回的值。
string String1()
{
var i = Random.Shared.Next(100);
return $"{i * i}";
}
在调用方法的时候,这个方法就可以被认为是一个值,可以用来给变量赋值,或是参与到表达式计算中。
string s1 = "随机数是" + String1();
流程控制
return
同时有结束方法的作用。在void
方法中不能在后面跟随值来返回,但可以单独使用来结束方法。
void Random1()
{
while (true)
{
int i = Random.Shared.Next(100);
Console.WriteLine("随机数是" + i);
if (i < 5)
{
return;
}
}
}
但是对于有返回值的方法,在必须使用return
来结束方法(如果方法能结束,即没有死循环)。
如果使用了流程控制语句,需要注意,你认为必定会执行的流程,编译器可能不会这么认为。
int Int3()
{
int i = 0;
while (i < 100)
{
i++;
return i;
}
return i;//尽管你认为这个方法必然会从上面的循环中返回。
//但对编译器来说那个循环可能完全不会执行。
//必须要在方法结束的地方另外写一个返回语句。
}
方法调用可以单独作为语句一行放置。
如果它有返回值而你不需要用到他的值,不需要做额外的处理,当作无返回值的方法就行。
引用返回值
引用变量也是有效的返回值类型。
ref int Int4()
{
int[] arr = new int[1];
return ref arr[0];
}
返回引用变量的方法可以取引用,也可以直接取值。
ref int i2 = ref Int4();
int i3 = Int4();
参数
声明方法的时候,可以在方法的括号里声明变量。
他们不需要赋值初始值。多个参数用逗号隔开,并且要写明类型,即便他们类型相同。
int Max1(int i1, int i2, int i3)
{
if (i1 > i2)
{
return i1 > i3 ? i1 : i3;
}
else
{
return i2 > i3 ? i2 : i3;
}
}
在调用方法的时候,需要在括号里填入对应的值。
使用逗号隔开,顺序和类型都对应上。
这些值将作为初始值赋值给这些参数。
int i4 = Max1(9, 6, 8);
Console.WriteLine(i4);//9
捕获与隔离
方法里可以直接使用外部的变量。这称为捕获变量。
任何对捕获变量的修改都会直接作用到它身上。
int i5 = 40;
Hello2();
Console.WriteLine(i5);//6
void Hello2()
{
i5 = 6;
}
而方法的参数可以声明和作用域外部同名的参数。
这样在方法内对它的修改只会改动到参数,不会影响到外部同名的变量。
int i6 = 40;
Hello3(0);
Console.WriteLine(i6);//40
void Hello3(int i5)
{
i5 = 6;
}
可选参数
参数可以赋值初始值,但必须是常量,或者default。
如果一个参数有初始值,那么他们之后的所有变量都要有初始值。
void Random2(int max = 100, int critical = 20)
{
if (Random.Shared.Next(max) < critical)
{
Console.WriteLine("暴击");
}
else
{
Console.WriteLine("没有暴击");
}
}
有初始值的参数在调用的时候可以不必填入初始值,
也可以正常填入值来覆盖预定义的初始值。
Random2();
Random2(40);
Random2(1000, 800);
不定长参数
如果参数的最后一个变量是数组,那么可以使用params
修饰。
(params
修饰的数组不能有默认值。所以不定长参数和可选参数不能同时使用)
int Min1(params int[] arr)
{
if (arr.Length == 0)
return 0;
int min = arr[0];
for (int i = 1; i < arr.Length; i++)
{
if (min < arr[i])
min = arr[i];
}
return min;
}
在调用的时候可以直接传入一个数组,
也可以以散装的形式填入。
Min1();
Min1(1);
Min1(6,9);
Min1(8,4,2);
命名参数
默认情况下,调用方法时需要按照参数在定义时的顺序来填入。
但如果指明这个值是给哪个参数的,那么可以乱序。
Hello4(b: 8, a: 9, d: 10);//输出9,8,6,10
void Hello4(int a, int b, int c = 6, int d = 40)
{
Console.WriteLine(a);
Console.WriteLine(b);
Console.WriteLine(c);
Console.WriteLine(d);
}
这种方式可以在有多个可选参数时,保持前面可选参数的默认值。
引用参数
参数可以设置为引用变量,在前面加上ref
。
void Hello5(ref int i)
{
i *= 2;
}
就像给引用变量赋值时一样,在调用方法时也需要加上ref
来把变量取指针。
int i7 = 10;
Hello5(ref i7);
Console.WriteLine(i7);//20
元组
元组类型
方法的返回类型是固定的,不能在某种条件下返回int,在另一种条件下返回bool。
但是可以把这两个数据打包一起返回。这需要一种包含多种数据,却是单独的类型。
除了数组可以打包多个同种类型数据外,元组可以打包固定数量和确定类型的数据。
元组的声明为把多个类型以逗号分隔,然后把他们加上小括号。
(string, int) Hello6((string, int) stu)
{
return stu;
}
打包和析构
将同样数量,顺序,类型的一堆值,以逗号分隔,打上括号,就能打包成一个元组类型。
(string, int) stu1 = ("小明", 18);//声明元组,并为元组赋值
(string name1, int age1) = stu1;//析构元组,声明两个变量接收他们
Console.WriteLine(name1);
Console.WriteLine(age1);
在元组类型后加上变量名,是一个元组类型的声明。
如果不加变量名,那么就是元组的析构。
元组会把打包的数据依次赋值给这些变量。
析构的时候可以声明变量,也可以对已有的变量覆盖值。
在析构时,可以使用下划线_
来舍弃一些值。
直接使用打包和析构,可以在单语句中交换变量的值。
int id2;
(_, int age2, id2) = ("小明", 18, 1006);//析构元组,舍弃一个值,声明一个变量,覆盖一个已有变量
(age2, id2) = (id2, age2);//交换age2和id2的值
元素命名
无命名
元组中的元素可以单独访问和修改。
任何情况下,元组中的元素都可以使用Item1
,Item2
,Item3
这种形式访问(从1开始计数)
(string, int) stu3 = ("小明", 18);
string name3 = stu3.Item1;
stu3.Item2 = 66;
类型命名
在声明元组类型时,可以为元素直接命名。
命名后依然可以使用Item1
,Item2
,Item3
进行访问,并且不可指定这些值作为名字。
(string name, int age) stu4 = ("小明", 18);
string name4 = stu4.name;
stu4.Item2 = 66;
值命名
如果使用var
来声明元组,可以在声明值的时候为值指定元素名。
var stu5 = (name: "小明", age: 18);
var name5 = stu5.name;
stu5.age = 88;
推断命名
如果你使用var
来声明元组,并且没有给值指定名字,但你使用的是变量,不是表达式,常量,方法。
那么会使用变量名来作为元素名。
string s1 = "hello";
var slength = (s1, s1.Length);
var length = slength.Length;
slength.s1 = "world";
可空值类型
在设置值类型的参数时,我们可能需要一些更特殊的值,而不是default。
例如,将一个int的默认值设置为0,我们无法分辨到底是没有填写保持默认值,
还是真的需要以0为参数来做处理。
我们可以使用可空值类型,在值类型后加上?
,他将可以接收null值。
int Random3(int max = 100, int? min = null)
{
if (min == null)
return Random.Shared.Next(max);
else
return Random.Shared.Next(min.Value, max);
}
可空值类型是他原本类型的包装类型。
他有两个属性,HasValue
和Value
。
HasValue
是判断这个值有没有值,效果和==null
一样。
如果有值,使用Value
访问他的值。但是如果是null
,这个访问会报错。
提升运算符
可空值类型继承了基础类型的运算符,这一特性是配合编译器联合工作的结果,我们无法复刻。
可空值类型在使用基础类型的运算符时,遵循以下规则:
- 如果没有
null
参与,按基础类型的方式执行。但返回值为可空值类型。 - 当使用关系运算符时
- 当双方为
null
且使用==
运算时,返回true
- 当仅一方为
null
且使用!=
运算时,返回true
- 否则返回
false
- 当双方为
- 如果对于逻辑运算符,如果是
true||y
,返回true
。如果是false&&y
返回false
- 其他二元运算符返回
null
空传播
在你访问一个值的内容时,可以在.
或[
前面加个?
,表示空传播。
只要左侧的值是null
,那么会阻止之后的所有内容访问,不会异常,并且返回null
string[]? arr1 = null;
string? s2 = arr1?[0];//不会阻止索引越界
int? i8 = s2?.Length;
var i9 = arr1?[0].Length;//只要arr1是null,后面的.Length也不会执行
如果是值类型,那么空传播就会返回可空值类型。
对可空值类型使用时,会直接访问到Value
的内容。
int? i10 = 2;
var s3 = i10.Value.ToString();
var s4 = i10?.ToString();
空容忍
对引用类型加?
不会发生什么事,因为他们本来就可以接收null
值。
只不过如果你不加?
,编译器会认为你不希望这个变量接收一个null
值,
在赋值的时候他会分析这些值,如果可能是null
就会向你发出警告。
如果你不希望提示这个警告,可以在值的右侧加上!
,表示我不在乎他是不是null
。
int? i11 = 10;
string s5 = i11?.ToString()!;
合并运算
使用x ?? y
运算符来简写x == null ? x : y
同样有他的赋值复合运算。
string s6 = null;
int i11 = s6?.Length ?? -1;
s6 ??= "hello";
如果合并运算表达式右侧的值不为null
,则这个表达式的值会被认为是不为null
的类型。
即对一个可空值类型使用合并运算并给默认值,会转为基础类型。
引用类型除了string
类型外,他们只能接收null
作为可选参数默认值。
使用合并运算以方便的为可选引用类型参数在方法内指定默认值。
int Random4(Random? random = null)
{
random ??= Random.Shared;
return random.Next(100);
}