在撰写了有关
Veeam学院的多篇文章之后,我们决定开设一个内部小厨房,并向您提供一些我们正在与学生一起分析的C#示例。 编译它们时,我们受到了这样的事实的指导,即我们的读者是新手开发人员,但对于经验丰富的程序员来说,也可能很有趣。 我们的目标是显示兔子洞的深度,同时解释C#内部结构的特征。
另一方面,我们将很高兴听到经验丰富的同事的意见,他们会指出我们示例中的缺陷,或者分享自己的示例。 他们喜欢在面试中使用这样的问题,因此可以肯定,我们都有话要说。
我们希望我们的选择对您有所帮助,有助于您恢复知识或只是微笑。

例子1
C#中的结构。 有了他们,即使是经验丰富的开发人员也经常会提出疑问,各种在线测试都经常使用这些问题。
我们的第一个示例是正念和对using块扩展到什么的知识的示例。 而且在面试中也是一个交流的话题。
考虑以下代码:
public struct SDummy : IDisposable { private bool _dispose; public void Dispose() { _dispose = true; } public bool GetDispose() { return _dispose; } static void Main(string[] args) { var d = new SDummy(); using (d) { Console.WriteLine(d.GetDispose()); } Console.WriteLine(d.GetDispose()); } }
Main方法将在控制台上打印什么?注意,SDummy是实现IDisposable接口的结构,因此SDummy类型的变量可以在using块中使用。
根据
C#语言规范,在编译时对重要类型的using语句扩展为try-finally块:
try { Console.WriteLine(d.GetDispose()); } finally { ((IDisposable)d).Dispose(); }
因此,在我们的代码中,在using块内调用GetDispose()方法,该方法返回布尔字段_dispose,尚未为d对象设置其值(仅在尚未调用的Dispose()方法中设置),因此返回值默认值为False。 接下来是什么?
然后最有趣。
在finally块中运行一行
((IDisposable)d).Dispose();
通常会导致拳击。 例如,
在这里 (在“结果”的右上角,首先选择C#,然后选择IL),这并不难看出:
在这种情况下,已经为另一个对象而不是d对象调用了Dispose方法。
运行我们的程序,看看该程序在控制台上确实显示“ False False”。 就是这么简单吗? :)
实际上,没有包装正在发生。 根据埃里克·利珀特(Eric Lippert)的观点,这样做是为了优化(请参见
此处和
此处 )。
但是,如果没有包装(它本身可能看起来很奇怪),为什么在屏幕上显示“ False False”而不是“ False True”,因为Dispose现在应该应用于同一对象?
这里不是那个!
看一下
C#编译器将我们的程序扩展为:
public struct SDummy : IDisposable { private bool _dispose; public void Dispose() { _dispose = true; } public bool GetDispose() { return _dispose; } private static void Main(string[] args) { SDummy sDummy = default(SDummy); SDummy sDummy2 = sDummy; try { Console.WriteLine(sDummy.GetDispose()); } finally { ((IDisposable)sDummy2).Dispose(); } Console.WriteLine(sDummy.GetDispose()); } }
有一个新变量sDummy2,已对其应用Dispose()方法!
这个隐藏变量来自哪里?
让我们再次转向
spec :
“ using(expression)statement”形式的using语句具有相同的三个可能扩展。 在这种情况下,ResourceType隐式是表达式的编译时类型...'resource'变量在嵌入式语句中不可访问,也不可见。
T.O. sDummy变量对于using块的嵌入式语句不可见且不可访问,并且此表达式内的所有操作均由另一个sDummy2变量执行。
结果,Main方法向控制台输出“ False False”,而不是“ False True”,因为许多第一次遇到此示例的人都相信。 在这种情况下,请务必记住没有包装,但是会创建其他隐藏变量。
总的结论是:可变值类型是邪恶的,最好避免。
这里考虑一个类似的例子。 如果主题很有趣,我们强烈建议您看一眼。
我要特别感谢
SergeyT对这个例子的宝贵意见。
例子2
构造函数及其调用顺序是任何面向对象编程语言的主要主题之一。 有时,这样的调用序列可能会令人惊讶,甚至更糟糕的是,在最意外的时刻甚至“填满”程序。
因此,请考虑MyLogger类:
class MyLogger { static MyLogger innerInstance = new MyLogger(); static MyLogger() { Console.WriteLine("Static Logger Constructor"); } private MyLogger() { Console.WriteLine("Instance Logger Constructor"); } public static MyLogger Instance { get { return innerInstance; } } }
假设此类具有一些我们需要支持日志记录的业务逻辑(功能现在并不那么重要)。
让我们看看MyLogger类中的内容:
- 指定了静态构造函数
- 有一个没有参数的私有构造函数
- 封闭的静态变量innerInstance定义
- 实例具有开放的静态属性,可用于与外界通信
为了便于分析此示例,我们在类的构造函数中添加了简单的控制台输出。
在类之外(不使用诸如反射之类的技巧),我们只能使用公共静态Instance属性,我们可以这样调用它:
class Program { public static void Main() { var logger = MyLogger.Instance; } }
该程序将输出什么?我们都知道,在访问类的任何成员之前(常量除外),都会调用静态构造函数。 在这种情况下,它仅在应用程序域内启动一次。
在我们的例子中,我们转到类成员-Instance属性,该属性应导致静态构造函数首先启动,然后将调用该类实例的构造函数。 即 该程序将输出:
静态记录器构造器
实例记录器构造器
但是,启动程序后,我们进入控制台:
实例记录器构造器
静态记录器构造器
怎么会这样 实例构造函数在静态构造函数之前起作用?!?
答:可以!
这就是为什么。
C#ECMA-334标准规定了静态类的以下内容:
17.4.5.1:“如果类中存在静态构造函数(第17.11节),则在执行该静态构造函数之前立即执行静态字段初始化程序。
...
17.11:...如果类包含带有初始化程序的任何静态字段,则这些初始化程序将在执行静态构造函数之前立即按文本顺序执行(这是免费翻译的意思:如果类中有静态构造函数,则在静态构造函数启动之前,立即开始静态字段的初始化。
...
如果该类包含带有初始化程序的任何静态字段,则在运行静态构造函数之前,将按照程序文本中的顺序启动此类初始化程序。)
在我们的例子中,静态字段innerInstance与初始化程序一起声明,初始化程序是类实例的构造函数。 根据ECMA标准,必须在调用静态构造函数之前调用初始化程序。 在我们的程序中会发生什么:作为静态字段的初始化程序的实例构造函数在静态构造函数之前被称为。 非常意外地同意。
请注意,这仅适用于静态字段初始化程序。 通常,在调用类实例的构造函数之前,将其称为静态构造函数。
例如,在这里:
class MyLogger { static MyLogger() { Console.WriteLine("Static Logger Constructor"); } public MyLogger() { Console.WriteLine("Instance Logger Constructor"); } } class Program { public static void Main() { var logger = new MyLogger(); } }
程序将输出到控制台:
静态记录器构造器
实例记录器构造函数

例子3
程序员通常必须编写辅助功能(实用程序,助手等),以使他们的生活更轻松。 通常,此类函数非常简单,通常只需要几行代码。 但是,您甚至可以跌跌撞撞地跌跌撞撞。
假设我们需要实现一个检查数字是否为奇数的函数(即该数字不能被2整除而没有余数)。
一个实现可能看起来像这样:
static bool isOddNumber(int i) { return (i % 2 == 1); }
乍一看,一切都很好,例如,对于数字5.7和11,我们期望得到True。
isOddNumber(-5)函数将返回什么?-5是奇数,但作为对我们函数的答案,我们得到False!
让我们找出原因。
根据
MSDN ,以下内容与%除法运算符的其余部分有关:
“对于整数操作数,a%b的结果是a-(a / b)* b产生的值”
在我们的情况下,对于a = -5,b = 2,我们得到:
-5%2 =(-5)-((-5)/ 2)* 2 = -5 + 4 = -1
但是-1始终不等于1,这说明了我们的结果为False。
%运算符对操作数的符号敏感。 因此,为了不接收此类“意外”,最好将结果与零进行比较,该零没有符号:
static bool isOddNumber(int i) { return (i % 2 != 0); }
或者获得一个单独的函数来检查奇偶校验并通过它实现逻辑:
static bool isEvenNumber(int i) { return (i % 2 == 0); } static bool isOddNumber(int i) { return !isEvenNumber(i); }
例子4
使用C#进行编程的每个人都可能会遇到LINQ,这对于处理集合,创建查询,过滤和聚合数据非常方便。
我们不会看LINQ的内幕。 也许我们会再做一次。
同时,请考虑一个小示例:
int[] dataArray = new int[] { 0, 1, 2, 3, 4, 5 }; int summResult = 0; var selectedData = dataArray.Select( x => { summResult += x; return x; }); Console.WriteLine(summResult);
此代码将输出什么?我们在屏幕上获得变量summResult的值,该值等于初始值,即 0。
为什么会这样呢?
并且因为LINQ查询的定义和此查询的启动是分别执行的两个操作。 因此,请求的定义并不意味着其启动/执行。
summResult变量在Select方法的匿名委托中使用:dataArray数组的元素顺序排序并添加到summResult变量中。
我们可以假设我们的代码将打印dataArray数组的元素之和。 但是LINQ不能那样工作。
考虑selectedData变量。 var关键字是“语法糖”,在许多情况下,它可以减小程序代码的大小并提高其可读性。 并且selectedData变量的实型实现IEnumerable接口。 即 我们的代码如下所示:
IEnumerable<int> selectedData = dataArray.Select( x => { summResult += x; return x; });
在这里,我们定义查询(Query),但是查询本身不会启动。 以类似的方式,您可以通过将SQL查询指定为字符串来使用数据库,但是要获取结果,请引用数据库并显式运行此查询。
也就是说,到目前为止,我们仅设置了一个请求,但尚未启动它。 这就是为什么summResult变量的值保持不变的原因。 例如,可以使用ToArray,ToList或ToDictionary方法启动查询:
int[] dataArray = new int[] { 0, 1, 2, 3, 4, 5 }; int summResult = 0;
此代码将已经显示变量summResult的值,该值等于dataArray数组的所有元素的总和,等于15。
我们知道了。 然后该程序将在屏幕上显示什么?
int[] dataArray = new int[] { 0, 1, 2, 3, 4, 5 };
实际上,groupedData变量(第3行)实现了IEnumerable接口,并实质上定义了对dataArray数据源的请求。 这意味着,要使匿名委托起作用并更改summResult变量的值,必须显式运行此请求。 但是我们的程序中没有这样的启动。 因此,summResult变量的值将仅在第2行中更改,并且我们无法在计算中考虑其他所有内容。
这样就很容易计算出变量summResult的值,即分别为15 + 7。 22
例子5
让我们马上说-我们在学院的演讲中没有考虑这个例子,但是有时我们在喝咖啡休息时间讨论它,而不是作为一个玩笑。
尽管从确定开发人员水平的角度来看这几乎不是指示性的事实,但我们在几个不同的测试中都遇到了这个示例。 也许是因为它具有多功能性,因为它在C和C ++中以及在C#和Java中都相同。
因此,让一行代码:
int i = (int)+(char)-(int)+(long)-1;
变量i的值是多少?答:1
您可能会认为这里在每种类型的大小(以字节为单位)上使用了数字算术,因为在进行类型转换时,在此意外遇到了符号“ +”和“-”。
在C#中,整数类型已知为4个字节长,8个长字符char 2。
然后,很容易想到我们的代码行将等效于以下算术表达式:
int i = (4)+(2)-(4)+(8)-1;
但是,事实并非如此。 为了使这样的错误推理造成混乱和指挥,可以更改示例,例如,如下所示:
int i = (int)+(char)-(int)+(long)-sizeof(int);
在此示例中,符号“ +”和“-”不用作二进制算术运算,而是用作一元运算符。 然后,我们的代码行仅仅是一系列显式类型转换和对一元操作的调用的混合,其编写方式如下:
int i = (int)(

有兴趣在Veeam学院学习吗?
现在在圣彼得堡有一套关于C#的春季课程,我们邀请所有人在
Veeam Academy网站上进行在线测试
。该课程于2019年2月18日开始,持续到5月中旬,并且将一如既往地完全免费。 想要通过入学考试的任何人都可以在学院的网站上注册:
academy.veeam.ru