C#的变迁史 - C# 2.0篇
在此重申一下,本文仅代表个人观点,如有不妥之处,还请自己辨别。
第一代的值类型装箱与拆箱的效率极其低下,特别是在集合中的表现,所以第二代C#重点解决了装箱的问题,加入了泛型。
1. 泛型 - 珍惜生命,远离装箱
集合作为通用的容器,为了兼容各种类型,不得已使用根类Object作为成员类型,这在C#1.0中带来了很大的装箱拆箱问题。为了C#光明的前途,这个问题必须要解决,而且要解决好。
C++模板是一个有用的启迪,虽然C++模板的运行机制不一样,但是思路确实是正确的。
带有形参的类型,也就是C#中的泛型,作为一种方案,解决了装箱拆箱,类型安全,重用集合的功能,防止具有相似功能的类泛滥等问题。泛型最大的战场就是在集合中,以List<T>,Queue<T>,Stack<T>等泛型版本的集合基本取代了第一代中非泛型版本集合的使用场合。当然除了在集合中,泛型在其他的地方也有广泛的用途,因为程序员都是懒的,重用和应对变化是计算机编程技术向前发展最根本的动力。
为了达到类型安全(比如调用的方法要存在),就必须有约定。本着早发现,早解决的思路,在编译阶段能发现的问题最好还是在编译阶段就发现,所以泛型就有了约束条件。泛型的约束常见的是下面几种:
a. 构造函数约束(使用new关键字),这个约束要求实例化泛型参数的时候要求传入的类必须有公开的无参构造函数。
b. 值类型约束(使用struct关键字),这个约束要求实例化泛型参数的类型必须是值类型。
c. 引用类型约束(使用class关键字),这个约束要求实例化泛型参数的类型必须是引用类型。
d. 继承关系约束(使用具体制定的类或接口),这个约束要求实例化泛型参数的类型必须是指定类型或是其子类。
当然了,泛型参数的约束是可以同时存在多个的,参看下面的例子:
public class Employee
{ }
class MyList<T, V> where T: Employee, IComparable<T>
where V: new()
{
}
如果不指定约束条件,那么默认的约束条件是Object,这个就不多讲了。
当使用泛型方法的时候,需要注意,在同一个对象中,泛型版本与非泛型版本的方法如果编译时能明确关联到不同的定义是构成重载的。例如:
public void Function1<T>(T a);
public void Function1<U>(U a);
这样是不能构成泛型方法的重载。因为编译器无法确定泛型类型T和U是否不同,也就无法确定这两个方法是否不同 public void Function1<T>(int x);
public void Function1(int x);
这样可以构成重载 public void Function1<T>(T t) where T:A;
public void Function1<T>(T t) where T:B;
这样不能构成泛型方法的重载。因为编译器无法确定约束条件中的A和B是否不同,也就无法确定这两个方法是否不同
使用泛型就简单了,直接把类型塞给形参,然后当普通的类型使用就可以了。例如:
List<int> ages = new List<int>();
ages.Add();
ages.Add();
ages.Remove();
2. 匿名函数delegate
在C# 2.0中,终于实例化一个delegate不再需要使用通用的new方式了。使用delegate关键字就可以直接去实例化一个delegate。这种没有名字的函数就是匿名函数。这个不知道是不是语法糖的玩意儿使用起来确实比先定义一个函数,然后new实例的方式要方便。不过最方便的使用方式将在下一版中将会到来。
delegate void TestDelegate(string s);
static void M(string s)
{
Console.WriteLine(s);
} //C# 1.0的方式
TestDelegate testDelA = new TestDelegate(M); //C# 2.0 匿名方法
TestDelegate testDelB = delegate(string s) { Console.WriteLine(s); };
谈到匿名函数,不得不说说闭包的概念。
如果把函数的工作范围比作一个监狱的话,函数内定义的变量就都是监狱中的囚犯,它们只能在这个范围内工作。一旦方法调用结束了,CLR就要回收线程堆栈空间,恢复函数调用前的现场;这些在函数中定义的变量就全部被销毁或者待销毁。但是有一种情况是不一样的,那就是某个变量的工作范围被人为的延长了,通俗的讲就像是某囚犯越狱了,它的工作范围超过了划定的监狱范围,这个时候它的生命周期就延长了。
闭包就是使用函数作为手段延长外层函数中定义的变量的作用域和生命周期的现象,作为手段的这个函数就是闭包函数。看一个例子:
class Program
{
static void Main(string[] args)
{
List<Action> actions = getActions();
foreach (var item in actions)
{
item.Invoke();
}
} static List<Action> getActions()
{
List<Action> actions = new List<Action>(); for (int i = ; i < ; i++)
{
Action item = delegate() { Console.WriteLine(i); };
actions.Add(item);
} return actions;
}
}
你可以试试运行这个例子,结果和你预想的一致吗?这个例子会输出5个5,而不是0到4,出现这个现象的原因就是闭包。getActions函数中的变量i被匿名函数引用了,它在getActions调用结束后还会一直存活到匿名函数执行结束。但是匿名函数是后面才调用的,执行它们的时候,i早就循环完毕,值是5,所以最终所有的匿名函数执行结果都是输出5,这是由闭包现象导致的一个bug。
要想修复这个由闭包导致的问题,方法基本上是破坏闭包引用,方式多种多样,下面是简单的利用值类型的深拷贝实现目的。
第一个方法:让闭包引用不再指向同一个变量
for (int i = ; i < ; i++)
{
int j = i;
Action item = delegate() { Console.WriteLine(j); };
actions.Add(item);
}
第二个方法:包上一层函数来构造新的作用域
static List<Action> getActions()
{
List<Action> actions = new List<Action>(); for (int i = ; i < ; i++)
{
Action item = ActionMethod(i);
actions.Add(item);
} return actions;
} static Action ActionMethod(int p)
{
return delegate()
{
Console.WriteLine(p);
};
}
闭包现象提醒我们使用匿名函数和3.0中的Lambda表达式时都要时刻注意变量的来源。
3. 迭代器
在C# 1.0中,集合实现迭代器模式是需要实现IEnumerable的,这个大家还记得吧,这个接口的核心就是GetEnumerator方法。实现这个接口主要是为了得到Enumerator对象,然后通过其提供的方法遍历集合(主要是Current属性和MoveNext方法)。自己去实现这些还是比较麻烦的,先需要定义一个Enumerator对象,然后在自定义的集合对象中还需要实现IEnumerable接口返回定义的Enumerator对象,于是一个新的语法糖就出现了: yield关键字。
在C# 2.0中,只需要在自定义的集合对象中还需要实现IEnumerable接口返回一个Enumerator对象就行了,这个创建Enumerator对象的工作就由编译器自己完成了。看一个简单的小例子:
public class Stack<T>:IEnumerable<T>
{
T[] items;
int count;
public void Push(T data){...}
public T Pop(){...}
public IEnumerator<T> GetEnumerator()
{
for(int i=count-;i>=;--i)
{
yield return items[i];
}
}
}
使用yield return创建一个Enumerator对象是不是很方便?编译器遇到yield return会创建一个Enumerator对象并自动维护这个对象。
当然了,多数时候foreach必要遍历集合中的每一个元素,这个时候使用yield return配合for循环枚举每个元素就可以了,但是有时候只需要返回满足条件的部分元素,这个时候就要结合yield break中断枚举了,看一下:
//使用yield break中断迭代:
for(int i=count-;i>=;--i)
{
yield return items[i]; if(items[i]>)
{
yield break;
}
}
4. 可空类型
这个特性我觉得又是把值类型设计成引用类型Object类子类后,微软生产的怪语法。空值是引用类型的默认值,0值是值类型的默认值。那么在某些场合,比如从数据库中的记录取到内存中以后,没有值代表的是空值,但是字段的类型却是值类型,怎么搞呢?于是整出了可空类型。当然了,这个问题可以通过在设计表的时候给字段设计一个默认值来解决,但是有的时候某些字段的设置默认值是没有意义的,比如年龄,0有意义吗?
可空类型的概念很简单,没什么可说的,不过一个相似的语法却让我感到很舒服:那就是"??"操作符。这是一个二元操作符,如果第一个操作数是空值,则执行第二个操作数代表的操作,并返回其结果。例如:
static void Main(string[] args)
{
int? age = null;
age = age ?? ;
Console.WriteLine(age);
}
5. 部分类与部分方法
这一特性还是比较好用的,终于不用把所有的内容挤到一起了,终于可以申明和实现相分离了,虽然好像以前也可以做到,但是现在这项权利也下放给人民群众了。partial关键字带来了这一切,也带来了一定的扩展性。这个特性也比较简单,就是使用partial关键字。编译的时候,这些文件中定义的部分类会被合并。部分方法是3.0的特性,不过没什么新意,就放到2.0一起说吧。
使用这个特性的时候需要注意:
a. 部分方法只能在部分类中定义。
b. 部分类和部分方法的签名应该是一致的。
c. partial用在类型上的时候只能出现在紧靠关键字 class、struct 或 interface 前面的位置。
d. public等访问修饰符必须出现在partial前面。
f. partial定义的东西必须是在同一程序集和模块中。
看一个简单的例子:
// File1.cs
namespace PC
{
partial class A
{
int num = ;
void MethodA() { }
partial void MethodC();
}
} // File2.cs
namespace PC
{
partial class A
{
void MethodB() { }
partial void MethodC() { }
}
}
这里需要注意一下MethodC的申明和定义是分开的就可以了。
还有一点,很多人认为partial破坏了类的封装性,实际上谈不上。因为一个类能分部,就说明类的设计者认为是需要保留这个扩展性的,所以后面的人才可以给这个类添加一些新的东西。
6. 静态类
这个特性也是比较符合实际情况的,很多情况下,某些对象只需要实例化一次,然后到处使用,单件模式是可以实现这个目的,现在静态类也是一个新的选择。
静态类只能含有静态成员,所以构造函数也是静态的,既然是静态的,那么它与继承就没什么关系了。静态类从首次调用的时候创建,一直到程序结束时销毁。
简单看一个小例子:
public static class A
{
static string message = "Message";
static A()
{
Console.WriteLine("Initialize!");
}
public static void M()
{
Console.WriteLine(message);
}
}
不过据经验讲,有没有静态构造函数对静态类的构造时间是有影响的,一个是出现在首次使用对象成员的时候,一个是程序集加载的时候,不过分清这个实在没什么意义,有兴趣的同学自己研究吧。
C#2.0的新特性绝不止这几个,但是对程序猿们影响比较大的都在这了,更多的就参看微软的MSDN吧。
C#的变迁史 - C# 2.0篇的更多相关文章
- C#的变迁史 - C# 4.0篇
C# 4.0 (.NET 4.0, VS2010) 第四代C#借鉴了动态语言的特性,搞出了动态语言运行时,真的是全面向“高大上”靠齐啊. 1. DLR动态语言运行时 C#作为静态语言,它需要编译以后运 ...
- C#的变迁史 - C# 3.0篇
C# 3.0 (.NET 3.5, VS2008) 第三代C#在语法元素基本完备的基础上提供了全新的开发工具和集合数据查询方式,极大的方便了开发. 1. WPF,WCF,WF 这3个工程类型奠定了新一 ...
- C#的变迁史 - C# 1.0篇
C#与.NET平台诞生已有10数年了,在每次重大的版本升级中,微软都为这门年轻的语言添加了许多实用的特性,下面我们就来看看每个版本都有些什么.老实说,分清这些并没什么太大的实际意义,但是很多老资格的. ...
- C#的变迁史 - C# 4.0 之多线程篇
在.NET 4.0中,并行计算与多线程得到了一定程度的加强,这主要体现在并行对象Parallel,多线程Task,与PLinq.这里对这些相关的特性一起总结一下. 使用Thread方式的线程无疑是比较 ...
- C#的变迁史 - C# 4.0 之线程安全集合篇
作为多线程和并行计算不得不考虑的问题就是临界资源的访问问题,解决临界资源的访问通常是加锁或者是使用信号量,这个大家应该很熟悉了. 而集合作为一种重要的临界资源,通用性更广,为了让大家更安全的使用它们, ...
- C#的变迁史 - C# 5.0 之调用信息增强篇
Caller Information CallerInformation是一个简单的新特性,包括三个新引入的Attribute,使用它们可以用来获取方法调用者的信息, 这三个Attribute在Sys ...
- C#的变迁史 - C# 5.0 之并行编程总结篇
C# 5.0 搭载于.NET 4.5和VS2012之上. 同步操作既简单又方便,我们平时都用它.但是对于某些情况,使用同步代码会严重影响程序的可响应性,通常来说就是影响程序性能.这些情况下,我们通常是 ...
- C#的变迁史 - C# 4.0 之并行处理篇
前面看完了Task对象,这里再看一下另一个息息相关的对象Parallel. Parallel对象 Parallel对象封装了能够利用多核并行执行的多线程操作,其内部使用Task来分装多线程的任务并试图 ...
- C#的变迁史 - C# 5.0 之其他增强篇
1. 内置zip压缩与解压 Zip是最为常用的文件压缩格式之一,也被几乎所有操作系统支持.在之前,使用程序去进行zip压缩和解压要靠第三方组件去支持,这一点在.NET4.5中已有所改观,Zip压缩和解 ...
随机推荐
- Nginx下WordPress的Rewrite
最近接触WP Super Cache,该插件要求固定链接必须是重写的,故用到Rewrite. 我的是这样配置的: /usr/local/nginx/conf/rewrite/wordpress.con ...
- Atititi.名字 姓名 name 起名naming spec 的构成结构规范v2 qc2.docx
Atititi.名字 姓名 name 起名naming spec 的构成结构规范v2 qc2.docx 1.1. 职业名 官职等 amir 阿米尔 放前面1 1.2. 本名1 1.3. 父名,祖名,一 ...
- salesforce 零基础开发入门学习(十五)salesforce中formula的使用(不含Date/Time)
本文参考官方的formula介绍PDF:https://resources.docs.salesforce.com/200/latest/en-us/sfdc/pdf/salesforce_usefu ...
- ASP.NET 如何判断当前请求是否是Ajax请求
/// <summary> /// Description:验证用户是否登陆 /// Author:xucixiao /// Date:2013- ...
- 重构Mybatis与Spring集成的SqlSessionFactoryBean(1)
一般来说,修改框架的源代码是极其有风险的,除非万不得已,否则不要去修改.但是今天却小心翼翼的重构了Mybatis官方提供的与Spring集成的SqlSessionFactoryBean类,一来是抱着试 ...
- Script Task 引用 package variable
Script Task 能够使用C#代码进行编程,许多复杂的逻辑都可以使用C# 脚本来实现,不仅灵活,而且强大. 1,能够传递package variable 给 Script Task ,并且Scr ...
- C# 索引器使用总结
1.索引器(Indexer): 索引器允许类或者结构的实例按照与数组相同的方式进行索引.索引器类似于属性,不同之处在于他们的访问采用参数. 最简单的索引器的使用 /// <summary> ...
- JAVA实现Excel的读写--poi
上一篇为大家介绍了通过xls.jar的方式生成Excel的方法,本篇就为大家再介绍一下通过poi方式实现Excel文件的读写操作,内容很简单,代码注释很清晰. 1.生成Excel文件: import ...
- 错误:document.getElementById("userForm").submit();Object is not a function
表单提交时发生的错误解决办法: 利用这种方法进行表单提交的时候,表单中的元素不能有 name="submit"的元素,否则该元素会与submit()方法造成混淆,导致错误!
- 哈夫曼树(一)之 C语言详解
本章介绍哈夫曼树.和以往一样,本文会先对哈夫曼树的理论知识进行简单介绍,然后给出C语言的实现.后续再分别给出C++和Java版本的实现:实现的语言虽不同,但是原理如出一辙,选择其中之一进行了解即可.若 ...