C# 试题 50:综合代码阅读与易错点
0981 以下代码输出什么?
难度: 实战
class Counter
{
public int N;
public int Tick => ++N;
}
var c = new Counter();
Console.WriteLine(c.Tick + c.Tick);
- A. 输出 2,属性值在同一表达式中只求值一次
- B. 输出 3,
Tick的 getter 被求值两次,副作用各发生一次 - C. 输出 1
- D. 编译失败,同一属性不能在表达式中出现两次
查看答案与解析
正确答案B
解析: Tick 是表达式体属性,每次访问 getter 都会执行 ++N。c.Tick + c.Tick 按从左到右求值两次 getter:第一次 N 变为 1 并返回 1,第二次变为 2 并返回 2,和为 3。属性访问不会自动缓存,求值次数就是访问次数。
关键点: 属性访问次数等于 getter 执行次数,副作用不可假设只发生一次。
0982 以下代码输出什么?
难度: 进阶
int[] a = { 10, 20, 30 };
int i = 0;
Console.WriteLine(a[i++] + a[i++]);
- A. 输出 20
- B. 输出 40
- C. 结果未定义,
i++的求值顺序由编译器决定 - D. 输出 30,C# 规定二元运算的操作数按从左到右求值
查看答案与解析
正确答案D
解析: C# 语言规范固定了求值顺序:二元运算符先求值左操作数再求值右操作数。a[i++] 先取 a[0](i 变 1),再取 a[1](i 变 2),结果为 10 + 20 = 30。这与 C/C++ 中“副作用顺序未指定”的语义不同。
关键点: C# 求值顺序是确定的从左到右,不存在 C/C++ 式未定义行为。
0983 以下代码输出什么?
难度: 进阶
bool F() { Console.Write("F"); return true; }
bool G() { Console.Write("G"); return false; }
bool r = F() || G() && F();
Console.WriteLine(r);
- A. 先输出
F,再输出True;&&优先级高于||,F()为true后直接短路 - B. 先输出
FGF,再输出True - C. 先输出
FG,再输出False - D. 先输出
F,再输出False
查看答案与解析
正确答案A
解析: && 优先级高于 ||,表达式等价于 F() || (G() && F())。F() 打印 F 并返回 true,|| 短路,右侧整体不再求值;最终结果为 true,由 Console.WriteLine 输出 True。
关键点: 优先级决定结合方式,短路决定哪些操作数真正被求值。
0984 以下代码输出什么?
难度: 实战
struct Counter
{
public int Value;
public void Increment() => Value++;
}
class Wrapper
{
public Counter Counter { get; set; } = new Counter();
}
var w = new Wrapper();
w.Counter.Increment();
Console.WriteLine(w.Counter.Value);
- A. 输出 1,
Increment修改了属性持有的结构体 - B. 编译失败,结构体不能定义实例方法
- C. 输出 0,
w.Counter的 getter 返回临时副本,Increment()修改的是副本 - D. 运行抛
NullReferenceException
查看答案与解析
正确答案C
解析: 结构体是值类型,属性 getter 按值返回副本。w.Counter.Increment() 实际是在 getter 返回的临时副本上调用 Increment,原字段 Value 保持 0,修改结果静默丢失。
关键点: 对属性返回的结构体调用修改方法等于修改副本,改动不生效。
0985 以下代码哪一行无法编译?
难度: 进阶
struct Point { public int X; }
Point[] arr = { new Point() };
arr[0].X = 5; // 第 1 行
var list = new List<Point> { new Point() };
list[0].X = 5; // 第 2 行
Console.WriteLine(arr[0].X);
- A. 第 1 行
- B. 第 2 行,
List<T>索引器返回的是元素副本,修改其成员属于修改“非变量”的返回值 - C. 两行都能编译
- D. 两行都无法编译
查看答案与解析
正确答案B
解析: 数组元素是变量,arr[0].X = 5 合法;List<T> 的索引器 getter 返回 T 的副本,list[0].X = 5 报 CS1612(不能修改非变量的返回值)。要改列表元素应整体替换元素(如 list[0] = new Point { X = 5 })或通过 ref 访问。
关键点: 数组元素可修改成员,List<T> 索引器返回副本时成员赋值是编译错误。
0986 以下代码输出什么?
难度: 进阶
struct Point
{
public int X;
public void Move(int dx) => X += dx;
}
class Box
{
readonly Point p;
public Box() { p = new Point { X = 1 }; }
public void Run()
{
p.Move(5);
Console.WriteLine(p.X);
}
}
new Box().Run();
- A. 输出 6
- B. 编译失败,
readonly字段不能调用实例方法 - C. 运行抛
InvalidOperationException - D. 输出 1,访问
readonly结构体字段的成员会先复制副本,Move修改的是副本
查看答案与解析
正确答案D
解析: 编译器无法保证实例方法不会修改结构体,因此对 readonly 字段 p 调用 Move 时先制作防御性副本,副本的 X 变成 6,原字段仍为 1。直接在方法里给成员赋值(p.X = ...)则是编译错误,二者要区分。
关键点: readonly 结构体字段的成员调用产生防御性副本,修改静默丢失。
0987 以下代码输出什么?
难度: 进阶
interface IInc { void Inc(); }
struct Counter : IInc
{
public int Value;
public void Inc() => Value++;
}
Counter c = new Counter();
((IInc)c).Inc();
((IInc)c).Inc();
Console.WriteLine(c.Value);
- A. 输出 0,每次装箱都产生独立副本,接口调用修改的是装箱对象
- B. 输出 2
- C. 输出 1
- D. 编译失败,结构体不能实现接口
查看答案与解析
正确答案A
解析: 结构体转换为接口必然装箱,((IInc)c) 每次创建新的装箱副本;Inc() 通过接口分派修改的是装箱对象里的字段,原变量 c 不受影响,两次调用后仍为 0。
关键点: 装箱即复制,接口分派修改的是副本而非原值类型变量。
0988 以下代码输出什么?
难度: 实战
var numbers = new List<int> { 1, 2, 3 };
var query = numbers.Where(n => n > 1);
numbers.Add(4);
Console.WriteLine(query.Count());
- A. 输出 2
- B. 输出 4
- C. 输出 3,
Where延迟执行,Count()枚举时读取的是当前源 - D. 运行抛
InvalidOperationException
查看答案与解析
正确答案C
解析: Where 返回延迟执行的查询,只保存源引用与谓词,Count() 触发枚举时源已包含 4,满足 n > 1 的是 2、3、4,共 3 个。查询结果取决于“枚举那一刻”的源状态。
关键点: 延迟执行意味着查询结果与枚举时的源内容绑定,而非创建查询时。
0989 以下代码输出什么?
难度: 实战
var funcs = new List<Func<int>>();
for (int i = 0; i < 3; i++)
{
funcs.Add(() => i * 10);
}
foreach (var f in funcs)
{
Console.Write(f() + " ");
}
- A. 输出
0 10 20 - B. 输出
30 30 30,for的循环变量只有一个实例,被所有闭包共享 - C. 输出
0 0 0 - D. 编译失败,循环变量不能被闭包捕获
查看答案与解析
正确答案B
解析: for 的循环变量在整段循环中只有一个实例,三个闭包捕获的是同一个 i;循环结束后 i == 3,每次调用都输出 30。C# 5+ 的 foreach 每次迭代使用独立变量,不受此问题影响。
关键点: for 闭包共享循环变量是经典陷阱,需局部副本或 foreach 规避。
0990 以下代码运行结果是什么?
难度: 实战
var list = new List<int> { 1, 2, 3 };
foreach (var x in list)
{
if (x == 2)
{
list.Add(4);
}
}
- A. 运行抛
InvalidOperationException,枚举期间修改集合触发版本检查 - B. 正常结束,
List<T>允许在foreach中追加元素 - C. 运行抛
NotSupportedException - D. 无限循环
查看答案与解析
正确答案A
解析: List<T> 的枚举器带有版本号,Add 会使版本号变化;foreach 在下一次 MoveNext 时检测到版本不一致,抛 InvalidOperationException。集合修改与枚举不能交错进行。
关键点: 枚举期间修改源集合是运行时异常,不是静默行为或死循环。
0991 以下代码输出什么?
难度: 进阶
var data = new List<int> { 1, 2, 3 };
int calls = 0;
var query = data.Select(x => { calls++; return x * 2; });
_ = query.ToList();
_ = query.ToList();
Console.WriteLine(calls);
- A. 输出 3
- B. 输出 9
- C. 编译失败,投影委托中不允许有副作用
- D. 输出 6,延迟查询每次枚举都重新执行投影
查看答案与解析
正确答案D
解析: Select 延迟执行且不缓存结果,每次 ToList() 都重新遍历源并执行投影委托;两次枚举共调用 6 次。若投影有副作用,枚举次数会直接放大副作用,应避免依赖。
关键点: 延迟查询多次枚举 = 多次重新执行,副作用次数与枚举次数成正比。
0992 以下代码输出什么?
难度: 实战
static string M()
{
try
{
return "try";
}
finally
{
Console.Write("finally ");
}
}
Console.WriteLine(M());
- A. 输出
try - B. 输出
try finally - C. 输出
finally try,finally在方法真正返回前执行 - D. 编译失败,
return与finally不能共存
查看答案与解析
正确答案C
解析: return 先计算返回值并保存,随后执行 finally,最后才把控制权交回调用方,因此先打印 finally 再输出 try。finally 的清理逻辑必然先于方法返回发生。
关键点: return 表达式先求值,但 finally 先执行,方法才算真正返回。
0993 以下代码输出什么?
难度: 进阶
static void M()
{
try
{
throw new InvalidOperationException("A");
}
finally
{
throw new FormatException("B");
}
}
try
{
M();
}
catch (FormatException e)
{
Console.WriteLine("Format: " + e.Message);
}
catch (InvalidOperationException e)
{
Console.WriteLine("Invalid: " + e.Message);
}
- A. 输出
Invalid: A - B. 输出
Format: B,finally抛出的异常取代在途的原始异常 - C. 依次输出
Invalid: A与Format: B - D. 运行抛
AggregateException
查看答案与解析
正确答案B
解析: 异常沿栈展开经过 finally 时,finally 又抛出 FormatException,新异常取代在途的 InvalidOperationException(原始异常不会自动成为内部异常),因此外层只捕获到 Format: B。
关键点: finally 中抛异常会覆盖原异常,清理逻辑中应避免抛出。
0994 以下代码输出什么?
难度: 实战
class R : IDisposable
{
public R() => throw new InvalidOperationException("ctor");
public void Dispose() => Console.Write("D");
}
try
{
using (var r = new R()) { }
}
catch (InvalidOperationException)
{
Console.Write("C");
}
- A. 输出
C,构造失败时资源从未取得,Dispose不会被调用 - B. 输出
DC - C. 输出
CD - D. 编译失败
查看答案与解析
正确答案A
解析: using 展开为 try/finally 包围语句体,而 new R() 的求值在 try 之前;构造函数抛异常时变量从未赋值,Dispose 不会被调用,异常被外层 catch 捕获并打印 C。
关键点: 构造失败不触发 Dispose,资源获取与释放的边界要按展开语义理解。
0995 以下代码输出什么?
难度: 进阶
static bool ShouldLog(Exception e)
{
Console.Write("L");
return false;
}
try
{
try
{
throw new InvalidOperationException();
}
catch (Exception e) when (ShouldLog(e))
{
Console.Write("C");
}
}
catch (InvalidOperationException)
{
Console.Write("P");
}
- A. 输出
C - B. 输出
LCP - C. 编译失败,
when过滤器必须返回bool - D. 输出
LP,过滤器返回false时跳过该catch,异常继续向外传播
查看答案与解析
正确答案D
解析: when 过滤器在 catch 体之前执行:ShouldLog 打印 L 并返回 false,该 catch 不匹配,异常继续向外传播,被外层 catch 捕获并打印 P。过滤器返回 true 才会进入 catch 体。
关键点: 过滤器返回 false 等于该 catch 不匹配,异常继续传播。
0996 以下代码输出什么?
难度: 实战
var data = new[]
{
("x", 1),
("y", 1),
("z", 0)
};
var sorted = data.OrderBy(t => t.Item2).Select(t => t.Item1);
Console.WriteLine(string.Join("", sorted));
- A. 输出
xyz - B. 输出
zyx - C. 输出
zxy,OrderBy是稳定排序,相等键保持原相对顺序 - D. 输出结果不确定
查看答案与解析
正确答案C
解析: 按文档契约,OrderBy 是稳定排序:相等键保持源顺序。键 0 的 z 排最前,键 1 的 x、y 保持原相对顺序,因此输出 zxy。Array.Sort 等不稳定排序在相等键上不能给出同样保证。
关键点: 稳定排序契约决定相等键的先后,不能想当然套用不稳定排序的直觉。
0997 以下代码运行结果是什么?
难度: 实战
var nums = new List<int> { 1, 2 };
int x = nums.SingleOrDefault();
Console.WriteLine(x);
- A. 输出 0
- B. 运行抛
InvalidOperationException,SingleOrDefault只在序列为空时返回默认值,多于一个元素仍抛异常 - C. 输出 1
- D. 运行抛
ArgumentException
查看答案与解析
正确答案B
解析: SingleOrDefault 的契约是:空序列返回默认值;恰好一个元素返回该元素;多于一个元素抛 InvalidOperationException。序列有两个元素,必然抛异常而不是返回 0,常见误解是把它当成“绝不抛异常”的版本。
关键点: SingleOrDefault 的默认值只对空序列生效,多个元素仍然抛异常。
0998 关于把可变结构体用作 Dictionary<TKey, TValue> 的键,哪项正确?
难度: 进阶
- A. 字典保存的是键的引用,插入后修改键字段不会影响查找
- B. 字典会自动对键做防御性复制,修改键字段不会生效
- C. 可变结构体无法实现
GetHashCode,不能作为键 - D. 插入后若修改了参与
GetHashCode/Equals的字段,哈希桶错位,后续查找可能失败或得到错误结果,因此可变结构体不宜作键
查看答案与解析
正确答案D
解析: 字典按键的哈希值定位桶。可变结构体插入后再修改参与 GetHashCode/Equals 的字段,哈希值变化,原桶位置已无法定位该键,查找可能失败或命中错误位置;且结构体按键值复制,任何“修改”都只影响副本。键的哈希稳定性是字典契约的前提。
关键点: 键在存续期间必须哈希不变,可变结构体违反该前提。
0999 关于 LINQ 的执行模式,哪项正确?
难度: 实战
- A. 延迟执行意味着查询每次被枚举都会重新执行(可能重新读取源),
ToList/ToArray等是立即物化 - B. 所有 LINQ 操作符都是延迟执行
- C.
Where在调用时立即过滤并返回新集合 - D.
OrderBy(...).ToList()中OrderBy会先执行并缓存结果,ToList只是读取缓存
查看答案与解析
正确答案A
解析: Where、Select、OrderBy 等返回延迟查询,枚举时才对源求值;ToList/ToArray/Count 等触发立即执行并物化结果。延迟查询没有“提前执行并缓存”的中间状态,每次枚举都是重算。
关键点: 延迟执行 = 每次枚举重算;要固定结果必须立即物化。
1000 阅读陌生 API 的契约时,哪项做法最可靠?
难度: 实战
- A. 方法名足以说明行为,例如
Get开头的方法一定没有副作用 - B. 只凭一次正常路径的测试结果即可确定全部边界行为
- C. 应结合签名、文档、异常约定与针对性测试确认边界,方法名与单一用例不足以证明行为
- D. 只要代码能编译,就说明调用符合 API 契约
查看答案与解析
正确答案C
解析: API 契约由签名、文档、异常约定和实际边界行为共同定义;方法名只是提示,一次正常路径测试无法覆盖空值、边界与异常路径。可靠做法是读文档契约并用针对性测试验证,例如 SingleOrDefault、OrderBy 这类“名字与语义有落差”的 API。
关键点: 名称与单一用例不足以下结论,契约要按签名、文档与边界测试综合判断。