变量
- 一个变量代表一个存储的位置,它的值是可以改变的
- 变量可以是
- 本地的 (方法内部)
- 参数(值类型、ref、out)(ref、out引用类型参数)
- 字段 (Field)
- 数组的元素
Stack栈 vs Heap堆
Stack栈
Stack(一块内存)存储本地变量(方法内)和参数
随着函数的进入和退出,Stack也会随之增大和缩小
public static int Factorial(int x)
{
if(x==0)
return 1;
return x * Factorial(x-1);
}
Heap堆
- Heap(一块内存),对象所在的地方(引用类型的实例)
- 当新的对象被创建后,它就会被分配在Heap上,到该对象的一个引用被返回
- 程序执行时,随着新对象的不断建立,heap会被慢慢的填满。运行时的gc会周期性的把对象从heap上面释放出来,所以不会导致内存耗尽
一旦一个对象不再被任何“存活”的东西所引用,那么它就可以被释放了
static void Main(string[] args)
{
StringBuilder ref1 = new StringBuilder("object1");
Console.WriteLine(ref1);
//ref1 引用的StringBuilder 就可以被 GC 清理了
StringBuilder ref2 = new StringBuilder("object2");
StringBuilder ref3 = ref2;
//ref2 引用的StringBuilder 还不可以被GC清理,因为ref3的引用是指向ref2的
Console.WriteLine(ref3);
}
内存
值类型的实例(和对象的引用)会放在变量声明时所在的内存块里
如果该实例是一个class的字段或数组的元素,那么它就在Heap(堆)上
GC
Static字段
在Heap上
它们会存活到应用程序域(AppDomain)停止
C#策略
确定赋值Definite Assignment
除非使用unsafe,否则在C#里无法访问为初始化的内存。
- Definite Assignment的三个含义:
- 本地变量在被读取之前必须被赋值
- 当方法被调用的时候,函数的参数必须被提供(除非是可选参数 Public void Test(int x,int y=1){})
- 其它的变量(字段、数组元素)会被运行时自动初始化。
默认值
所有类型的实例都有默认值
预定义类型的默认值就是内存按位归零的结果
Type | Default value |
---|---|
All reference types | Null |
All numeric and enum typers | 0 |
char type | ‘\0’ |
bool type | false |
通过default关键字来获取任何类型的默认值
int x = default;
string s = default;
自定义值类型(struct)的默认值就是该自定义类型定义的每个字段的默认值
参数
一个方法可以有多个参数(parameters),参数(parameters)定义了必须为该方法提供的参数(arguments)
static void Main()
{
Fool(8); // 9 :argument
}
static void Fool(int p) // p :parameters
{
p = p + 1 ;
Console.WriteLine(p);
}
中文一般prarmeters翻译为形参,argument翻译为实参
参数传递的方式
Parameter modifier | Passed by | Variable must be definitely assigned |
---|---|---|
(None) | Value | Going in |
ref | Reference | Going in |
out | Reference | Going out |
Variable must be definitely assigned (当进入方法的时候变量必须被赋值)
out 要求在方法结束前被赋值
按值传递arguments
默认情况下,按值传递arguments
当传进方法是把arguments的值复制了一份
static int x;
static void Main()
{
ValueType(x);
System.Console.WriteLine(x); // x is 0
}
static void ValueType(int x)
{
x += 1;
System.Console.WriteLine(x); // x is 1
}
按值传递引用类型的arguments
赋值的是引用,不是对象(引用类型当参数传递的时候,是把引用类型的指向传递过去了,当参数在方法中进行各种操作的时候就等于对传递前的参数进行各种操作)
static void Main()
{
StringBuilder sb = new StringBuilder();
sb.Append("Hello ");
SbAddend(sb);
Console.WriteLine(sb); // print : Hello Word!
}
static void SbAddend(StringBuilder sb)
{
sb.Append("Word!");
}
按引用传递ref
想要按引用传递,可以使用ref参数修饰符
static void Main()
{
int x = 1;
// x在使用前必须赋值,不然会报:(使用了未赋值的局部变量“x”)的错误
// 传递的时候必须在x前加上 ref
RefAdd(ref x);
System.Console.WriteLine(x); // x is now 2
}
static void RefAdd(ref int x)
{
x+=1;
}
按引用传递out
和ref差不多,除了:
进入函数前不需要被赋值
离开函数前必须被赋值
通常用来从方法返回多个值
static void Main()
{
string a,b;
Split("Stevie Ray Vaughan",out a,out b);
System.Console.WriteLine(a); // Stevie Ray
System.Console.WriteLine(b); // Vaughan
}
static void Split(string name, out string firstNames, out string lastName)
{
int i = name.LastIndexOf(' ');
firstNames = name.Substring(0, i);
lastName = name.Substring(i + 1);
}
out变量
从C#7开始,调用方法时,可以使用out临时声明变量
当调用的方法有多个out参数时,你不需要其中一些out参数,可以使用下划线”_”来discard(弃用)它们。
static void Main()
{
Split("Stevie Ray Vaughan",out string a,out string b); // C#7特性,可以直接用out临时声明变量
System.Console.WriteLine(a); // Stevie Ray
System.Console.WriteLine(b); // Vaughan
}
Split("Stevie Ray Vaughan",out string a,out _); // C#7特性,可以使用下划线"_"不适用这个参数
按引用类型进行传递的含义
当你按引用传递arguments的时候,相当于给现有变量的存储位置起了个别名,而不是创建了一个新的存储位置。
params修饰符
- 可以在方法的最后一个参数使用params参数修饰符
- 可以接受任意数量的该类型的参数
- 参数(parameters形参)类型必须是数组
- 也可使用数组作为arguments(实参)
static void Main()
{
int[] paras = {1, 2, 3, 4, 5, 6, 7 };
Sum(1, 2, 3, 4, 5, 6, 7); // 用params修饰的可以随便填写同一类型的参数
Sum(paras); // 也可以直接传相同类型的数组
}
static void Sum(params int[] ints)
{
int sum = 0;
for (int i = 0; i < ints.Length; i++)
{
sum += i;
}
System.Console.WriteLine(sum);
}
可选参数
- 从C#4.0开始,方法、构造函数、索引器都可以声明可选参数
可选参数需要在声明的时候提供默认值
void Foo(int x=23){Console.WriteLine(x);}
调用时可以不填写可选的parameters(形参)
- Foo();相当于Foo(23);编译器在调用方法的时候把23写死进去
- 往public方法里添加可选参数,若该方法被其它Assembly(程序集)调用,那么两个Assemblies都需要重新编译,就和添加了一个必填参数一样的
- 可选参数的默认值必须是常量表达式或拥有无参构造函数的值类型
- 可选参数不可以使用ref和out
- 必填参数必须在可选参数前面(方法声明时和方法调用时),(写在后面编译器直接不给通过。。)。
- 例外是:params的参数依然放在最后一位
命名参数
可以不按位置来区别arguments(实参)
使用名称来定位arguments
命名的arguments可以按任意顺序来填写
Arguments表达式被计算出的顺序和他们在调用地出现的顺序一致
可混合使用按位参数和命名参数
按位参数必须放在命名参数前
可混合使用可选参数和命名参数
static void Main()
{
KxParas(x: 1, y: 1); // 1,1
KxParas(x: 1, y: 2); // 1,2
KxParas(y: 1, x: 2); // 2,1
}
static void KxParas(int x, int y)
{
System.Console.WriteLine($"{x},{y}");
}
对于自增变量传参时的变化(从左至右的传递)
static void Main()
{
int a = 0;
KxParas(y: ++a, x: --a); // 0,1
int b = 0;
KxParas(x: --b, y: ++b); // -1,0
}
static void KxParas(int x, int y)
{
System.Console.WriteLine($"{x},{y}");
}
ref Locals
C#7,可以定义一个本地变量,它引用了数组的一个元素或对象的一个字段
int[] numbers = { 0, 1, 2, 3, 4 };
ref int numRef = ref numbers[2];
numRef就是对numbers[2]的引用,修改numRef的时候,就是修改numbers[2]这个元素
(自己的理解:就是类似于将numbers[2]这个值的地址赋给numRef,对numRef进行操作就等于对numbers[2]这个元素就行操作,引用类型?好处?)
RefLocal的目标必须是数组的元素,字段,本地变量。不可以是属性
常用于微优化场景,通常与ref returns联合使用
static void Main()
{
int[] numbers = { 1, 2, 3, 4 };
ref int numRef = ref numbers[2];
numRef *= 10;
System.Console.WriteLine(numRef); // 30
System.Console.WriteLine(numbers[2]); // 30
}
ref returns
可以从方法返回ref local,这就叫做ref return
static string X = "Old Value";
static ref string GetX() => ref X; // 这个方法返回一个ref
static void Main()
{
ref string xRef = ref GetX();
xRef = "New Value";
System.Console.WriteLine(X);
}
var(隐式类型var)
隐式强类型本地变量
声明和初始化变量通常一步完成,如果编译器能从初始化表达式推断出类型,就可以使用var
少用,会降低代码可读性
static void Main()
{
var x ="Hello";
var y = new System.Text.StringBuilder();
var z = (float)Math.PI;
}