C#的变迁史05 - C# 4.0篇

C# 4.0 (.NET 4.0, VS2010)

  第四代C#借鉴了动态语言的特性,搞出了动态语言运行时,真的是全面向“高大上”靠齐啊。


1. DLR动态语言运行时

  C#作为静态语言,它需要编译以后运行,在编译的过程中,编译器要检查语法的正确性和类型的安全性,这是一个静态查找(编译时查找)的过程。确实,在运行之前发现问题总比在运行时发型问题要好的多,早发现早治疗嘛!但是这样做有时候会带来一些麻烦,比如类型在编译时无法获得时。

  看网上经典的一个例子:动态计算器。

  假设有一个计算器,它所在的程序集是动态加载进来的;当我们需要使用这个计算器计算数据时,通常是使用反射的方式:

复制代码

object calc = GetCalculator();  
Type calcType = calc.GetType();  
object res = calcType.InvokeMember("Add",  BindingFlags.InvokeMethod, null,  new object[] { 10, 20 });  
int sum = Convert.ToInt32(res); 

复制代码

  不错,很好,可是有点麻烦。

  还有一种情况出现在Office程序中,例如给某单元格赋值:

((Excel.Range)excel.Cells[1, 1]).Value2= "Hello";  

  因为Cells返回的类型要想使用Value2属性,需要进行类型转换。

  在上面的这些例子中,因为C#是静态语言类型,就是强类型语言,所以要使用某个类的成员,就需要在编译的时候保证使用的是这个类的实例,或者是用反射。在这些场合下,写这样的代码无疑是不够优雅的。

  在C# 4.0中,这个情况会得到改善,因为这个版本的C#天生支持运行时类型查找,那就是CLR级别的DLR特性与语法级别的dynamic类型。

  在4.0中,程序可以直接写成:

复制代码

// Calculator
dynamic calc = GetCalculator();  
int sum = calc.Add(10, 20);// Office
excel.Cells[1, 1].Value2 = "Hello";

复制代码

  使用dynamic定义的对象,CLR将不再进行静态查找,而是交给DLR在运行时进行动态的查找。这样的做法无疑是拓展了程序的扩展性和约束,例如此前要实现某些公共的行为,通常是需要先定义一个接口,拥有这个行为的对象实现这个接口,这样在程序中就可以针对这个接口进行编程。但是使用dynamic以后,这个接口就可以省掉了,直接使用成员就可以了。

  dynamic作为新的类型,可以用在任何类型允许出现的场合。当然也可以用在变量的传递中,Runtime会自动选择一个最匹配的方法。使用dynamic类型,就可以不去关心对象的实例是来源于COM, IronPython, HTML DOM或者反射,只要知道有什么方法可以调用就可以了,剩下的工作就交给DLR了。

  其实在某种程度上,可以认为dynamic类型是object类型的一个特殊版本,除了具有object所有的特征外,还指出了对象可以动态地使用。选择是否使用动态行为很简单,任何对象都可以隐式转换为dynamic,直到运行时才动态绑定。反之,从dynamic到任何其他类型都存在隐式转换。例如:

dynamic d = 7;
int i = d; 

  上面所谓的动态操作,不仅是指方法调用,字段和属性访问、索引器和运算符调用,甚至委托调用都可以动态地调用,例如:

复制代码

dynamic d = GetDynamicObject(…); 
d.M(7);
d.f = d.P; 
d["one"] = d["two"];
int i = d + 3; 
string s = d(5,7);

复制代码

  同时,任何动态操作的结果本身也是dynamic类型的,这个自然是很好理解。

  但是需要注意dynamic也不是万能的:

1). 目前动态查找不支持扩展方法的调用(可能在未来的版本的C#中会提供支持)。

2). 匿名方法和Lambda表达式不能转换为dynamic,也就是说dynamic d = x=>x;是不合法的,事实上lambda表达式也不能转成object。一样的道理,因为lambda表达式会在上下文环境下要么被编译器解释成委托类型,要么被解释成表达式树,但是如果上下文缺乏类型信息,编译器会无法解析。

  所以总的说来,还是那一条,编译器能认识的地方(能编译,能推断)就可以使用dynamic。

  dynamic的实现是基于IDynamicObject接口和DynamicObject抽象类。而动态方法、属性的调用都被转为了GetMember、Invoke等方法的调用。如果想在自己的代码中实现一个动态类型对象,可以继承DynamicObject类,并实现自己的若干get和set方法。看一个网上的例子:

复制代码

public class MyClass:DynamicObject
{public override bool TryInvokeMember(InvokeMemberBinder binder, object[] args, out object result){result =  binder.Name;return true; }
}

复制代码

  上述代码在尝试invoke某个方法的时候直接返回该方法的名字。于是下面的代码将输出方法名:

dynamic d = new MyClass();
Console.WriteLine(d.AnyMember());

  最后来谈谈DLR(Dynamic Language Runtime),它是.Net 4.0中一组全新的API。对于C#,DLR提供了Microsoft.CSharp.RuntimeBinder命名空间,它为C#提供了强大的运行时互操作(COM,Ironpython等)能力,DLR具有优秀的缓存机制,对象一旦被成功绑定,CLR在下一次调用的时候就可以直接对确定类型的对象进行操作,而不必再通过DLR去查找了。


2. 命名参数(Named Parameter)与可选参数(Optional Parameter)

  这两个概念并没有什么联系,不过却经常纠缠在一起。

  先来看后面的这位仁兄,Optional Parameter与Required Parameter是相对的概念,老实说其它的语言中早就有了,只不过C#中到了4.0中才支持这个特性。看一个例子就会明白:

复制代码

// 方法声明
public void M(int x, int y = 5, int z = 7);  // 方法使用方式
M(1, 2, 3); // 这个没什么可说的  
M(1, 2);     // 等价于(1, 2, 7)   
M(1);         // 等价于 M(1, 5, 7)  

复制代码

  本质上,可选参数就是提供了函数参数的默认值,如果调用时不提供该参数的值,则取给定的默认值,就是这么简单。它的出现确实极大的减少了使用函数重载的情况,否则的话每种使用默认值的调用情况都得使用重载实现。

  有几点需要说明:

1).可选参数必须有个编译时常量作为其默认值。如果是除String之外的引用类型(包括那个特殊的dynamic类型),默认值只能是null。下面的声明是不能通过编译的

static void Foo(int a, String s = "i'm a string", dynamic b = 2, MyClass c = new MyClass())

2).可选参数必须从右往左出现在参数列表中(必须后出现),可选参数右边的参数(如果有的话)必须是可选参数。下面的声明是不能通过编译的

static void Foo(String s = "i'm a string", int a, dynamic b = null, MyClass c = null)

3).可选参数不仅适用于普通的方法,还适用于构造器,索引器中,本质上它们没有什么不同。

  说完可选参数,下面再谈谈命名参数。说的简单一点,命名参数就是在调用的时候指定了参数定义时的名称的参数,这样就能帮助有效编译器匹配实参和形参。

  对于Required Parameter来说,调用的时候是严格按顺序来的,自然不需要指定参数名称了,但是指定了因为没关系。

  对于Optional Parameter来说,调用时方法时,由于这些参数中某些参数使用了默认值,所以可能不出现在调用的实参列表中的,为了避免会歧义,这时就需要使用形参名称来避免误会。这是命名参数使用最多的场合。

  而且使用了命名参数后,编译器可以很轻松的配对实参和形参,所以参数的顺序就可以不按照定义时的顺序了。

  看一组简单的例子:

复制代码

static void Main(string[] args)
{M(1, "A");M(x: 1, s: "A");M(s:"B", x:2);M1(3, s1:"Hi", s2:"Dong");M1(4, s2:"Dong", s1: "Hi");M1(5, s2: "Dong");
}static void M(int x, string s)
{Console.WriteLine(x);Console.WriteLine(s);
}static void M1(int x, string s1 = "Hello", string s2="DXY")
{Console.WriteLine(x);Console.WriteLine(s1 + " " + s2);
}

复制代码

  从上面可以看出,命名参数在避免歧义方面使用起来还是很方便的。


3. 协变与逆变

  协变与逆变(Covariance and contravariance)指的是基类与子类实例之间满足条件的隐式转换;简单来讲,所谓协变(Covariance)是指把类型从“小”升到“大”,比如从子类升级到父类;逆变则是指从“大”变到“小”,比如从父类降级到子类。

  它们是面向对象语言的基本特征之一,与继承机制息息相关。继承机制与面向对象设计五大原则之一的里氏替换原则都要求所有使用基类的地方都可以使用子类,这包括传递参数的时候。

  此外,好的面向对象设计也要求对象满足“宽进严出”,概括的说就是传进对象的对象要求要宽松一点,流出对象的对象要求要严格一点。

  具体来说,这一原则体现在对象的初始化上,就是可以把子类的实例付给基类。这一原则体现在对象方法的实现上,就是方法的参数尽量使用能使用的基类(宽进,这样方法的灵活性就很好,所有基类的子类都可以传入该方法),方法的返回值尽量使用能使用的子类(严出,这样方法的返回值就容易明确方法的目的性,使用该方法的对象更容易处理返回值)。这一原则体现在代理delegate上,就是实例化代理类型的时候,使用的方法的参数可以是代理定义中参数类型的基类,使用的方法的返回值可以是代理定义中返回值类型的子类。比较绕吧,有时候我自己都会用错词,看看例子就会很清楚了:

复制代码

delegate BaseResult MethodHandler(BaseParameter p);class Program
{static void Main(string[] args){// 协变: DerivedParameter -> ParameterParameter p = new DerivedParameter();// 完全匹配MethodHandler m1 = M1;// 逆变: 参数Parameter -> BaseParameterMethodHandler m2 = M2;// 协变: 返回值DerivedResult -> BaseResultMethodHandler m3 = M3;}static BaseResult M1(BaseParameter p) { return null; }static BaseResult M2(Parameter p) { return null; }static DerivedResult M3(BaseParameter p) { return null; }
}abstract class Parameter { }
class BaseParameter : Parameter { }
class DerivedParameter : BaseParameter { }abstract class Result { }
class BaseResult : Result { }
class DerivedResult : BaseResult { }

复制代码

  虽然大部分情况下,我们直接初始化的类型都与定义的类型时完全匹配的,但是上面的例子中的初始化其实都是合法的,不仅合法,而且通常使用协变和逆变的方式其实更符合面向接口编程的方式。

  在4.0这个版本之前,泛型是不能满足协变和逆变的特性的,有兴趣的同学可以验证一下,虽然没什么实际意义。在4.0中,协变和逆变得到了改善,在泛型中的得到了进一步的支持;这是和out与in两个关键字密切相关的:out修饰的泛型参数只能作为函数的输出,in修饰的泛型参数只能作为函数的输入参数类型,使用了这两个关键字的泛型就满足协变和逆变的特性。看下面的例子:

复制代码

delegate T ActionHandler<out T>();
class Program
{static void Main(string[] args){ActionHandler<string> a1 = M;ActionHandler<object> a2 = a1;IEnumerable<string> strings = new List<string>();IEnumerable<object> objects = strings;}static string M() { return null; }
}

复制代码

  例子中自定义泛型使用了out修饰泛型参数,因而例子中的用法是合法的。.NET Framework中的很多泛型都添加了这个修饰符,例如:

复制代码

.Net4.0中使用out/in声明的Interface:
System.Collections.Generic.IEnumerable< out T>  
System.Collections.Generic.IEnumerator< out T>  
System.Linq.IQueryable< out T>  
System.Collections.Generic.IComparer< in T>  
System.Collections.Generic.IEqualityComparer< in T>  
System.IComparable< in T> .Net4.0中使用out/in声明的Delegate:
System.Func< in T, …, out R>  
System.Action< in T, …>  
System.Predicate< in T>  
System.Comparison< in T>  
System.EventHandler< in T> 

复制代码

  其实,做这些本质上都是要在保证运行时类型安全的前提下提高代码的可重用性和灵活性。正是因为这个原因,IList<T>泛型没有添加out/in声明,所以下面的用法是不对的:

IList<string> strings = new List<string>();   
IList<object> objects = strings; 

  究其根本原因,还是因为上面的使用无法保证运行时类型安全。例如下面的代码:

objects[0] = 5;   
string s = strings[0]; 

  这会允许将int插入strings列表中,然后将其作为string取出,这会破坏类型安全,所以IList这种允许修改元素的集合没有添加out/in声明。

  C#4.0中的协变和逆变使得泛型编程时的类型转换更加自然,不过要注意的是上面所说的协变和逆变都只作用于引用类型之间,例如,IEnumerable<int>不能作为IEnumerable<object>使用,因为从int到object的转换是装箱转换,而不是引用转换。而且在目前的泛型语法中,只能对泛型接口和委托使用协变和逆变。此外,一个泛型参数T只能是in或者是out,你如果即想你的委托参数逆变又想返回值协变,是做不到的。

  好了,4.0的主要特性就这些,不再啰嗦了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/438134.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

REVERSE-PRACTICE-BUUCTF-30

REVERSE-PRACTICE-BUUCTF-30[RCTF2019]DontEatMe[b01lers2020]little_engine[NPUCTF2020]你好sao啊[MRCTF2020]Shit[RCTF2019]DontEatMe exe程序&#xff0c;运行后输入&#xff0c;无壳&#xff0c;用ida分析 交叉引用字符串来到sub_401260函数&#xff0c;读取输入&#xff…

C#的变迁史06 - C# 4.0 之并行处理篇

前面看完了Task对象&#xff0c;这里再看一下另一个息息相关的对象Parallel。 Parallel对象 Parallel对象封装了能够利用多核并行执行的多线程操作&#xff0c;其内部使用Task来分装多线程的任务并试图将它们分配到不同的内核中并行执行。请注意“试图”这个词&#xff0c;Par…

REVERSE-PRACTICE-BUUCTF-31

REVERSE-PRACTICE-BUUCTF-31[羊城杯 2020]login[羊城杯 2020]Bytecode[羊城杯 2020]babyre[ACTF新生赛2020]fungame[羊城杯 2020]login exe程序&#xff0c;运行后输入&#xff0c;无壳&#xff0c;ida分析 没找到主要逻辑&#xff0c;在字符串窗口看到一些“py”的字样&#…

C#的变迁史07 - C# 4.0 之线程安全集合篇

作为多线程和并行计算不得不考虑的问题就是临界资源的访问问题&#xff0c;解决临界资源的访问通常是加锁或者是使用信号量&#xff0c;这个大家应该很熟悉了。 而集合作为一种重要的临界资源&#xff0c;通用性更广&#xff0c;为了让大家更安全的使用它们&#xff0c;微软为我…

PWN-PRACTICE-BUUCTF-1

PWN-PRACTICE-BUUCTF-1test_your_ncripwarmup_csaw_2016ciscn_2019_n_1test_your_nc 附件的main函数直接system("/bin/sh")&#xff0c;nc直接连即可cat flag rip main函数中&#xff0c;gets函数读取一行会造成栈溢出 构造payload覆盖rip&#xff0c;使得return…

C#的变迁史08 - C# 5.0 之并行编程总结篇

C# 5.0 搭载于.NET 4.5和VS2012之上。 同步操作既简单又方便&#xff0c;我们平时都用它。但是对于某些情况&#xff0c;使用同步代码会严重影响程序的可响应性&#xff0c;通常来说就是影响程序性能。这些情况下&#xff0c;我们通常是采用异步编程来完成功能&#xff0c;这在…

REVERSE-PRACTICE-CTFSHOW-1

REVERSE-PRACTICE-CTFSHOW-1逆向签到题re2逆向4逆向5逆向签到题 ida打开即可得到明文flag re2 附件是一个加密过的flag文本和勒索病毒exe 运行程序&#xff0c;输入1&#xff0c;回车&#xff0c;直接退出&#xff0c;ida分析 选项1的逻辑为&#xff0c;打开flag.txt和enfl…

C#的变迁史09 - C# 5.0 之调用信息增强篇

Caller Information CallerInformation是一个简单的新特性&#xff0c;包括三个新引入的Attribute&#xff0c;使用它们可以用来获取方法调用者的信息&#xff0c; 这三个Attribute在System.Runtime.CompilerServices命名空间下&#xff0c;分别叫做CallerMemberNameAttribute&…

REVERSE-PRACTICE-CTFSHOW-2

REVERSE-PRACTICE-CTFSHOW-2re3红包题 武穆遗书数学不及格flag白给re3 main函数&#xff0c;分析可知&#xff0c;将输入的字符串按十六进制转成数字&#xff0c;写到v5&#xff0c;赋给v17[6] 当i等于6时&#xff0c;v16会加上输入的值&#xff0c;然后进入循环&#xff0c;最…

C#的变迁史10 - C# 5.0 之其他增强篇

1. 内置zip压缩与解压   Zip是最为常用的文件压缩格式之一&#xff0c;也被几乎所有操作系统支持。在之前&#xff0c;使用程序去进行zip压缩和解压要靠第三方组件去支持&#xff0c;这一点在.NET4.5中已有所改观&#xff0c;Zip压缩和解压功能已经内置于框架本身。这个功能使…

REVERSE-PRACTICE-CTFSHOW-3

REVERSE-PRACTICE-CTFSHOW-3签退神光签到baby_gay签退 .pyc文件&#xff0c;uncompyle6反编译&#xff0c;得到python源码&#xff0c;分析写在源码注释中 先变表base64&#xff0c;再凯撒加密&#xff0c;向后移动2位 import string c_charset string.ascii_uppercase str…

REVERSE-PRACTICE-CTFSHOW-4

REVERSE-PRACTICE-CTFSHOW-4encodeEasyBJD hamburger competitionJustREencode elf文件&#xff0c;upx脱壳&#xff0c;ida分析 交叉引用字符串"Please input your flag:"&#xff0c;来到sub_804887C函数 输入经过三次变换&#xff0c;先是变表base64&#xff0c;…

CSS 基础框盒模型介绍

当对一个文档进行布局&#xff08;lay out&#xff09;的时候&#xff0c;浏览器的渲染引擎会根据标准之一的 CSS 基础框盒模型&#xff08;CSS basic box model&#xff09;&#xff0c;将所有元素表示为一个个矩形的盒子&#xff08;box&#xff09;。CSS 决定这些盒子的大小…

REVERSE-PRACTICE-CTFSHOW-5

REVERSE-PRACTICE-CTFSHOW-5re2_归心Mud[吃鸡杯]ezmore[吃鸡杯]有手就行re2_归心 exe程序&#xff0c;运行后要求输入flag&#xff0c;ida分析 函数窗没找到主逻辑函数&#xff0c;shiftF12看字符串窗口 发现有java/lang/String&#xff0c;com/exe4j/runtime/WinLauncher等字…

PWN-PRACTICE-BUUCTF-2

PWN-PRACTICE-BUUCTF-2pwn1_sctf_2016jarvisoj_level0ciscn_2019_c_1[第五空间2019 决赛]PWN5pwn1_sctf_2016 main函数中执行vuln函数 fgets限制了输入的长度&#xff0c;不足以构成栈溢出 通过将输入中的字符"I"替换成"you"&#xff0c;增加长度&#xf…

PWN-PRACTICE-BUUCTF-3

PWN-PRACTICE-BUUCTF-3[OGeek2019]babyropciscn_2019_n_8get_started_3dsctf_2016jarvisoj_level2[OGeek2019]babyrop 简单的ret2libc&#xff0c;构造rop main函数中读取一个随机数到buf中&#xff0c;传入sub_804871F 用"\x00"来绕过strlen和strncmp&#xff0c;b…

c#中常用集合类和集合接口之接口系列【转】

常用集合接口系列&#xff1a;http://www.cnblogs.com/fengxiaojiu/p/7997704.html 常用集合类系列&#xff1a;http://www.cnblogs.com/fengxiaojiu/p/7997541.html 大多数集合都在System.Collections&#xff0c;System.Collections.Generic两个命名空间。其中System.Colle…

PWN-PRACTICE-BUUCTF-4

PWN-PRACTICE-BUUCTF-4ciscn_2019_en_2bjdctf_2020_babystacknot_the_same_3dsctf_2016[HarekazeCTF2019]baby_ropciscn_2019_en_2 这题和ciscn_2019_c_1一模一样 栈溢出ret2libc&#xff0c;encrypt函数里的异或运算不用管 from pwn import * context.log_level"debug&…

PWN-PRACTICE-BUUCTF-5

PWN-PRACTICE-BUUCTF-5jarvisoj_level2_x64ciscn_2019_n_5others_shellcodeciscn_2019_ne_5jarvisoj_level2_x64 这题和[HarekazeCTF2019]baby_rop几乎一模一样 from pwn import * #context.log_level"debug" ioremote(node4.buuoj.cn,27023) elfELF(./level2_x64)…

Scrum敏捷开发沉思录

计算机科学的诞生&#xff0c;是世人为了用数字手段解决实际生活中的问题。随着时代的发展&#xff0c;技术的进步&#xff0c;人们对于现实世界中的问题理解越来越深刻&#xff0c;描述也越来越抽象&#xff0c;于是对计算机软件的需求也越来越高&#xff0c;越来越复杂&#…