.NET C# 八股文 代码阅读(一)
目录
1 两种获10000个数的方式,哪种效率更高?为什么?
// 方式一:
List<int> ints = new List<int>();
for (int i = 0; i < 10000; i++)
{
ints.Add(i);
}
// 方式二:
float[] floats = new float[10000];
for (int i = 0; i < 10000; i++)
{
floats[i] = i;
}
方式二,因为List会不断扩容,扩容时会反复拷贝造成性能损耗
2 请说出以下代码AB谁先打印,AB打印的值分别为多少?
static int GetInt()
{
int i = 10;
try
{
return i;
}
finally
{
i = 11;
Console.WriteLine("第B处 i= " + i);
}
}
static void Main(string[] args)
{
int i = GetInt();
Console.WriteLine("第A处 i= " + i);
}
// 输出:
// 第B处 i= 11
// 第A处 i= 10
步骤如下:
- 定义局部变量
i
并赋值为 10。 - 进入
try
块,准备返回i
的值,即 10。 - 在返回之前,进入
finally
块,将i
赋值为 11,并打印"第B处 i= " + i
。 - 返回值已经在
try
块中确定为 10,即使在finally
块中修改了i
的值,也不会影响返回值。
finally
块的特点是不论 try
中是正常返回还是异常抛出,它总会在 try
块的返回语句执行前执行,但它对已经确定的返回值不会有影响。因此,GetInt
方法的返回值仍然是 10。
class Test
{
public int i = 10;
}
static Test GetObj()
{
Test t = new Test();
try
{
return t;
}
finally
{
t.i = 11;
Console.WriteLine("第B处 i= " + t.i);
}
}
static void Main(string[] args)
{
Test t = GetObj();
Console.WriteLine("第A处 i= " + t.i);
}
// 输出:
// 第B处 i= 11
// 第A处 i= 11
因为 GetObj
返回的是一个 Test
对象,是引用类型,所以返回的实际上是一个指向 Test
实例的地址;
所以在 try
返回之前,在 finally
中对这个对象进行了修改,而 try
返回之后,Main
中再根据这个地址找到这个 Test
实例,自然也是 finally
修改之后的实例。
这两种情况的不同实际上是对 “赋值” 与 “修改” 的混淆,如果想依旧实现与上面 GetInt
相同的输出,代码应该改成如下:
class Test
{
public int i = 10;
}
static Test GetObj()
{
Test t = new Test();
try
{
return t;
}
finally
{
t = new Test();
t.i = 11;
Console.WriteLine("第B处 i= " + t.i);
}
}
static void Main(string[] args)
{
Test t = GetObj();
Console.WriteLine("第A处 i= " + t.i);
}
// 输出:
// 第B处 i= 11
// 第A处 i= 10
3 关于值类型与引用类型、装箱与拆箱,以下代码会输出什么?
interface IA
{
public int id { get; set; }
public string name { get; set; }
public int[] children { get; set; }
}
struct A : IA
{
public int id { get; set; }
public string name { get; set; }
public int[] children { get; set; }
}
class B
{
public int id { get; set; }
public string name { get; set; }
public int[] children { get; set; }
}
static void DoA (A a)
{
a.id=6;
a.name="Bob";
a.children[0]=7;
}
static void DoB (B b)
{
b.id=6;
b.name="Bob";
b.children[0]=7;
}
static void Main(string[] args)
{
var a = new A();
a.name = "Alick";
a.children = new int[] { 1, 2, 3 };
DoA(a);
Console.WriteLine($"a - name: {a.name}, id: {a.id}, children0: {a.children[0]}");
IA ia = a;
DoIA(ia);
Console.WriteLine($"ia - name: {ia.name}, id: {ia.id}, children0: {ia.children[0]}");
Console.WriteLine($"a - name: {a.name}, id: {a.id}, children0: {a.children[0]}");
var b = new B();
b.name = "Alick";
b.children = new int[] { 1, 2, 3 };
DoB(b);
Console.WriteLine($"b - name: {b.name}, id: {b.id}, children0: {b.children[0]}");
}
// 输出:
// a - name: Alick, id: 0, children0: 7
// ia - name: Bob, id: 6, children0: 7
// a - name: Alick, id: 0, children0: 7
// b - name: Bob, id: 6, children0: 7
代码分析
-
结构体
A
和接口IA
-
结构体
A
实现了接口IA
。 -
以
A
声明变量时,是一个值类型,因此在传递给方法时会进行值复制。 -
以
IA
声明变量时,会进行装箱(boxing),使其变成对象,因此在传递给方法时传递的是引用。
-
-
类
B
B
是一个引用类型,因此在传递给方法时会传递引用。
-
DoA
方法-
DoA
直接操作结构体A
。 -
由于
A
是值类型,传递给DoA
时会创建一个副本。 -
修改副本的
id
和name
不会影响原来的A
,但修改数组(引用类型)的内容会影响原数组。 -
name
是string
类型,也是引用类型,但对string
类型的修改都会创建新的字符串,所以相当于是赋予了新的引用地址,并没有修改name
原来的字符串实例。
-
-
DoIA
方法-
DoIA
操作的是接口IA
。 -
虽然传递的是实现了
IA
的结构体A
,但是接口会装箱(boxing)这个结构体,使其变成对象。 -
装箱后的修改,会影响装箱后的对象,但不会影响原来的结构体实例。
-
-
DoB
方法-
DoB
操作的是类B
。 -
由于
B
是引用类型,传递的是引用,方法中的修改会影响原对象。
-
4 关于变量作用域,以下代码会输出什么?
Action action = null;
for (int i = 0; i < 10; i++)
{
action += () => Console.WriteLine(i);
}
action.Invoke();
// 输出:
// 0
// 0
// 0
// 0
// 0
// 0
// 0
// 0
// 0
// 0
分析代码:
-
定义一个空的
Action
委托:Action action = null;
-
使用
for
循环添加匿名方法到action
:for (int i = 0; i < 10; i++) { action += () => Console.WriteLine(i); }
在每次循环中,都会将一个新的匿名方法(Lambda 表达式)添加到
action
委托中,这个匿名方法会打印变量i
的值。 -
调用
action
委托:action.Invoke();
由于 Lambda 表达式捕获的是变量 i
的引用,而不是它的当前值,当 action.Invoke()
被调用时,for
循环已经完成,变量 i
的值已经变成了 10。因此,所有的匿名方法在被执行时,都会打印当前 i
的值,也就是 10。
Action action = null;
for (int i = 0; i < 10; i++)
{
int localI = i; // 引入一个新的局部变量
action += () => Console.WriteLine(localI);
}
action.Invoke();
// 输出:
// 0
// 1
// 2
// 3
// 4
// 5
// 6
// 7
// 8
// 9
这样每个匿名方法都会捕获自己的 localI
变量,这个变量在每次循环迭代时都有自己唯一的值。