.NET面试题解析(05)-常量、字段、属性、特性与委托

本文涉及的产品
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
简介: 转自:http://www.cnblogs.com/anding/p/5255492.html  常见面试题目:1. const和readonly有什么区别?2. 哪些类型可以定义为常量?常量const有什么风险?3.

转自:http://www.cnblogs.com/anding/p/5255492.html  

常见面试题目:

1. const和readonly有什么区别?

2. 哪些类型可以定义为常量?常量const有什么风险?

3. 字段与属性有什么异同?

4. 静态成员和非静态成员的区别?

5. 自动属性有什么风险?

6. 特性是什么?如何使用?

7. 下面的代码输出什么结果?为什么?

List<Action> acs = new List<Action>(5);
for (int i = 0; i < 5; i++)
{
    acs.Add(() => { Console.WriteLine(i); });
}
acs.ForEach(ac => ac());

8. C#中的委托是什么?事件是不是一种委托?

  字段与属性的恩怨

微笑  常量

常量的基本概念就不细说了,关于常量的几个特点总结一下:

  • 常量的值必须在编译时确定,简单说就是在定义是设置值,以后都不会被改变了,她是编译常量。
  • 常量只能用于简单的类型,因为常量值是要被编译然后保存到程序集的元数据中,只支持基元类型,如int、char、string、bool、double等。
  • 常量在使用时,是把常量的值内联到IL代码中的,常量类似一个占位符,在编译时被替换掉了。正是这个特点导致常量的一个风险,就是不支持跨程序集版本更新;

关于常量不支持跨程序集版本更新,举个简单的例子来说明:

public class A
{
    public const int PORT = 10086;

    public virtual void Print()
    {
        Console.WriteLine(A.PORT);
    }
}

上面一段非常简单代码,其生产的IL代码如下,在使用常量变量的地方,把她的值拷过来了(把常量的值内联到使用的地方),与常量变量A.PORT没有关系了。假如A引用了B程序集(B.dll文件)中的一个常量,如果后面单独修改B程序集中的常量值,只是重新编译了B,而没有编译程序集A,就会出问题了,就是上面所说的不支持跨程序集版本更新。常量值更新后,所有使用该常量的代码都必须重新编译,这是我们在使用常量时必须要注意的一个问题。

  • 不要随意使用常量,特别是有可能变化的数据;
  • 不要随便修改已定义好的常量值;

image

生气 补充一下枚举的本质

接着上面的const说,其实枚举enum也有类似的问题,其根源和const一样,看看代码你就明白了。下面的是一个简单的枚举定义,她的IL代码定义和const定义是一样一样的啊!枚举的成员定义和常量定义一样,因此枚举其实本质上就相当是一个常量集合。

public enum EnumType : int
{
    None=0,
    Int=1,
    String=2,
}

image

吐舌笑脸 关于字段

字段本身没什么好说的,这里说一个字段的内联初始化问题吧,可能容易被忽视的一个小问题(不过好像也没什么影响),先看看一个简单的例子:

public class SomeType
{
    private int Age = 0;
    private DateTime StartTime = DateTime.Now;
    private string Name = "三体";
}

定义字段并初始化值,是一种很常见的代码编写习惯。但注意了,看看IL代码结构,一行代码(定义字段+赋值)被拆成了两块,最终的赋值都在构造函数里执行的。

image

那么问题来了,如果有多个构造函数,就像下面这样,有多半个构造函数,会造成在两个构造函数.ctor中重复产生对字段赋值的IL代码,这就造成了不必要的代码膨胀。这个其实也很好解决,在非默认构造函数后加一个“:this()”就OK了,或者显示的在构造函数里初始化字段。

public class SomeType
{
    private DateTime StartTime = DateTime.Now;

    public SomeType() { }

    public SomeType(string name)
    {                
    }
}

大笑 属性的本质

属性是面向对象编程的基本概念,提供了对私有字段的访问封装,在C#中以get和set访问器方法实现对可读可写属性的操作,提供了安全和灵活的数据访问封装。我们看看属性的本质,主要手段还是IL代码:

public class SomeType
{
    public int Index { get; set; }

    public SomeType() { }
}

image

上面定义的属性Index被分成了三个部分:

  • 自动生成的私有字段“<Index>k__BackingField”
  • 方法:get_Index(),获取字段值;
  • 方法:set_Index(int32 'value'),设置字段值;

因此可以说属性的本质还是方法,使用面向对象的思想把字段封装了一下。在定义属性时,我们可以自定义一个私有字段,也可以使用自动属性“{ get; set; } ”的简化语法形式。

使用自动属性时需要注意一点的是,私有字段是由编译器自动命名的,是不受开发人员控制的。正因为这个问题,曾经在项目开发中遇到一个因此而产生的Bug:

这个Bug是关于序列化的,有一个类,定义很多个(自动)属性,这个类的信息需要持久化到本地文件,当时使用了.NET自带的二进制序列化组件。后来因为一个需求变更,把其中一个字段修改了一下,需要把自动属性改为自己命名的私有字段的属性,就像下面实例这样。测试序列化到本地没有问题,反序列化也没问题,但最终bug还是被测试出来了,问题在与反序列化以前(修改代码之前)的本地文件时,Index属性的值丢失了!!!

private int _Index;
public int Index
{
    get { return _Index; }
    set { _Index = value; }
}

因为属性的本质是方法+字段,真正的值是存储在字段上的,字段的名称变了,反序列化以前的文件时找不到对应字段了,导致值的丢失!这也就是使用自动属性可能存在的风险。

  委托与事件

什么是委托?简单来说,委托类似于 C或 C++中的函数指针,允许将方法作为参数进行传递。

  • C#中的委托都继承自System.Delegate类型;
  • 委托类型的声明与方法签名类似,有返回值和参数;
  • 委托是一种可以封装命名(或匿名)方法的引用类型,把方法当做指针传递,但委托是面向对象、类型安全的;

疑惑 委托的本质——是一个类

.NET中没有函数指针,方法也不可能传递,委托之所可以像一个普通引用类型一样传递,那是因为她本质上就是一个类。下面代码是一个非常简单的自定义委托:

public delegate void ShowMessageHandler(string mes);

看看她生产的IL代码

image

我们一行定义一个委托的代码,编译器自动生成了一堆代码:

  • 编译器自动帮我们创建了一个类ShowMessageHandler,继承自System.MulticastDelegate(她又继承自System.Delegate),这是一个多播委托;
  • 委托类ShowMessageHandler中包含几个方法,其中最重要的就是Invoke方法,签名和定义的方法签名一致;
  • 其他两个版本BeginInvoke和EndInvoke是异步执行版本;

因此,也就不难猜测,当我们调用委托的时候,其实就是调用委托对象的Invoke方法,可以验证一下,下面的调用代码会被编译为对委托对象的Invoke方法调用:

private ShowMessageHandler ShowMessage;

//调用
this.ShowMessage("123");

image

疑惑 .NET的闭包

闭包提供了一种类似脚本语言函数式编程的便捷、可以共享数据,但也存在一些隐患。

题目列表中的第7题,就是一个.NET的闭包的问题。

List<Action> acs = new List<Action>(5);
for (int i = 0; i < 5; i++)
{
    acs.Add(() => { Console.WriteLine(i); });
}
acs.ForEach(ac => ac()); // 输出了 5 5 5 5 5,全是5?这一定不是你想要的吧!这是为什么呢?

上面的代码中的Action就是.NET为我们定义好的一个无参数无返回值的委托,从上一节我们知道委托实质是一个类,理解这一点是解决本题的关键。在这个地方委托方法共享使用了一个局部变量i,那生成的类会是什么样的呢?看看IL代码:

image

共享的局部变量被提升为委托类的一个字段了:

  • 变量i的生命周期延长了;
  • for循环结束后字段i的值是5了;
  • 后面再次调用委托方法,肯定就是输出5了;

那该如何修正呢?很简单,委托方法使用一个临时局部变量就OK了,不共享数据:

List<Action> acss = new List<Action>(5);
for (int i = 0; i < 5; i++)
{
    int m = i;
    acss.Add(() => { Console.WriteLine(m); });
}
acss.ForEach(ac => ac()); // 输出了 0 1 2 3 4

至于原理,可以自己探索了!

  题目答案解析:

1. const和readonly有什么区别?

const关键字用来声明编译时常量,readonly用来声明运行时常量。都可以标识一个常量,主要有以下区别: 
1、初始化位置不同。const必须在声明的同时赋值;readonly即可以在声明处赋值,也可以在构造方法里赋值。 
2、修饰对象不同。const即可以修饰类的字段,也可以修饰局部变量;readonly只能修饰类的字段 。 
3、const是编译时常量,在编译时确定该值,且值在编译时被内联到代码中;readonly是运行时常量,在运行时确定该值。 
4、const默认是静态的;而readonly如果设置成静态需要显示声明 。 
5、支持的类型时不同,const只能修饰基元类型或值为null的其他引用类型;readonly可以是任何类型。

2. 哪些类型可以定义为常量?常量const有什么风险?

基元类型或值为null的其他引用类型,常量的风险就是不支持跨程序集版本更新,常量值更新后,所有使用该常量的代码都必须重新编译。

3. 字段与属性有什么异同?

  • 属性提供了更为强大的,灵活的功能来操作字段
  • 出于面向对象的封装性,字段一般不设计为Public
  • 属性允许在set和get中编写代码
  • 属性允许控制set和get的可访问性,从而提供只读或者可读写的功能 (逻辑上只写是没有意义的)
  • 属性可以使用override 和 new

4. 静态成员和非静态成员的区别?

  • 静态变量使用 static 修饰符进行声明,静态成员在加类的时候就被加载(上一篇中提到过,静态字段是随类型对象存放在Load Heap上的),通过类进行访问。
  • 不带有static 修饰符声明的变量称做非静态变量,在对象被实例化时创建,通过对象进行访问 。
  • 一个类的所有实例的同一静态变量都是同一个值,同一个类的不同实例的同一非静态变量可以是不同的值 。
  • 静态函数的实现里不能使用非静态成员,如非静态变量、非静态函数等。

5. 自动属性有什么风险?

因为自动属性的私有字段是由编译器命名的,后期不宜随意修改,比如在序列化中会导致字段值丢失。

6. 特性是什么?如何使用?

特性与属性是完全不相同的两个概念,只是在名称上比较相近。Attribute特性就是关联了一个目标对象的一段配置信息,本质上是一个类,其为目标元素提供关联附加信息,这段附加信息存储在dll内的元数据,它本身没什么意义。运行期以反射的方式来获取附加信息。使用方法可以参考:http://www.cnblogs.com/anding/p/5129178.html

7. 下面的代码输出什么结果?为什么?

List<Action> acs = new List<Action>(5);
for (int i = 0; i < 5; i++)
{
    acs.Add(() => { Console.WriteLine(i); });
}
acs.ForEach(ac => ac());

输出了 5 5 5 5 5,全是5!因为闭包中的共享变量i会被提升为委托对象的公共字段,生命周期延长了

8. C#中的委托是什么?事件是不是一种委托?

什么是委托?简单来说,委托类似于 C或 C++中的函数指针,允许将方法作为参数进行传递。

  • C#中的委托都继承自System.Delegate类型;
  • 委托类型的声明与方法签名类似,有返回值和参数;
  • 委托是一种可以封装命名(或匿名)方法的引用类型,把方法当做指针传递,但委托是面向对象、类型安全的;

事件可以理解为一种特殊的委托,事件内部是基于委托来实现的。

相关文章
|
19天前
|
存储 Java
深入探讨了Java集合框架中的HashSet和TreeSet,解析了两者在元素存储上的无序与有序特性。
【10月更文挑战第16天】本文深入探讨了Java集合框架中的HashSet和TreeSet,解析了两者在元素存储上的无序与有序特性。HashSet基于哈希表实现,添加元素时根据哈希值分布,遍历时顺序不可预测;而TreeSet利用红黑树结构,按自然顺序或自定义顺序存储元素,确保遍历时有序输出。文章还提供了示例代码,帮助读者更好地理解这两种集合类型的使用场景和内部机制。
33 3
|
21天前
|
存储 算法 Java
解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用
在Java中,Set接口以其独特的“无重复”特性脱颖而出。本文通过解析HashSet的工作原理,揭示Set如何利用哈希算法和equals()方法确保元素唯一性,并通过示例代码展示了其“无重复”特性的具体应用。
38 3
|
23天前
|
缓存 JavaScript 前端开发
Vue3与Vue2生命周期对比:新特性解析与差异探讨
Vue3与Vue2生命周期对比:新特性解析与差异探讨
72 2
|
26天前
|
XML JSON API
ServiceStack:不仅仅是一个高性能Web API和微服务框架,更是一站式解决方案——深入解析其多协议支持及简便开发流程,带您体验前所未有的.NET开发效率革命
【10月更文挑战第9天】ServiceStack 是一个高性能的 Web API 和微服务框架,支持 JSON、XML、CSV 等多种数据格式。它简化了 .NET 应用的开发流程,提供了直观的 RESTful 服务构建方式。ServiceStack 支持高并发请求和复杂业务逻辑,安装简单,通过 NuGet 包管理器即可快速集成。示例代码展示了如何创建一个返回当前日期的简单服务,包括定义请求和响应 DTO、实现服务逻辑、配置路由和宿主。ServiceStack 还支持 WebSocket、SignalR 等实时通信协议,具备自动验证、自动过滤器等丰富功能,适合快速搭建高性能、可扩展的服务端应用。
86 3
|
2天前
|
编译器 C# 开发者
C# 9.0 新特性解析
C# 9.0 是微软在2020年11月随.NET 5.0发布的重大更新,带来了一系列新特性和改进,如记录类型、初始化器增强、顶级语句、模式匹配增强、目标类型的新表达式、属性模式和空值处理操作符等,旨在提升开发效率和代码可读性。本文将详细介绍这些新特性,并提供代码示例和常见问题解答。
13 7
C# 9.0 新特性解析
|
1天前
|
C# 开发者
C# 10.0 新特性解析
C# 10.0 在性能、可读性和开发效率方面进行了多项增强。本文介绍了文件范围的命名空间、记录结构体、只读结构体、局部函数的递归优化、改进的模式匹配和 lambda 表达式等新特性,并通过代码示例帮助理解这些特性。
8 2
|
7天前
|
存储 NoSQL MongoDB
MongoDB面试专题33道解析
大家好,我是 V 哥。今天为大家整理了 MongoDB 面试题,涵盖 NoSQL 数据库基础、MongoDB 的核心概念、集群与分片、备份恢复、性能优化等内容。这些题目和解答不仅适合面试准备,也是日常工作中深入理解 MongoDB 的宝贵资料。希望对大家有所帮助!
|
4天前
|
PHP 开发者
PHP 7新特性深度解析及其最佳实践
【10月更文挑战第31天】本文将深入探讨PHP 7带来的革新,从性能提升到语法改进,再到错误处理机制的变革。我们将通过实际代码示例,展示如何高效利用这些新特性来编写更加健壮和高效的PHP应用。无论你是PHP新手还是资深开发者,这篇文章都将为你打开一扇窗,让你看到PHP 7的强大之处。
|
4天前
|
安全 编译器 PHP
PHP 8新特性解析与实践应用####
————探索PHP 8的创新功能及其在现代Web开发中的实际应用
|
7天前
|
Kubernetes Cloud Native 调度
云原生批量任务编排引擎Argo Workflows发布3.6,一文解析关键新特性
Argo Workflows是CNCF毕业项目,最受欢迎的云原生工作流引擎,专为Kubernetes上编排批量任务而设计,本文主要对最新发布的Argo Workflows 3.6版本的关键新特性做一个深入的解析。

热门文章

最新文章

推荐镜像

更多