C#与C++的发展历程第一 - 由C#3.0起

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了C#与C++的发展历程第一 - 由C#3.0起相关的知识,希望对你有一定的参考价值。

俗话说学以致用,本系列的出发点就在于总结C#和C++的一些新特性,并给出实例说明这些新特性的使用场景。前几篇文章将以C#的新特性为纲领,并同时介绍C++中相似的功能的新特性,最后一篇文章将总结之前几篇没有介绍到的C++11的新特性。

C++从11开始被称为现代C++(Modern C++)语言,开始越来越不像C语言了。就像C#从3.0开始就不再像Java了。这是一种超越,带来了开发效率的提高。

一种语言的特性一定是与这种语言的类型和运行环境是分不开的,所以文章中说C#的新特性其中也包括新的.NET Framework和CLR(DLR)对C#的支持。

由于C#2.0除了泛型,迭代器yield,foreach等与Java等有所不同,其它没有特别之处,所以本系列将直接从C#3.0开始。

 

C#3.0 (.NET Framework 3.5, CLR 2.0 下同)

 

C# 对象初始化器与集合初始化器

在对象初始化器出现之前,我们实例化一个对象并赋值的过程代码看起来是很冗余的。比如有这样一个类:

1
2
3
4
5
6
class Plant
{
    string Name{get;set;}
    string Category{get;set;}
    int ImageId{get;set;}
}

实例化并赋值的代码如下:

1
2
3
4
Plant peony = new Plant();
Peony.Name = "牡丹";
Peony.Category= "芍药科";
Peony.ImageId=6;

如果我们需要多次实例化并赋值,为了节省赋值代码,可以提供一个构造函数:

1
2
3
4
5
6
Plant(string Name,string Category,int ImageId)
{
    Name = name;
    Category=category;
    ImageId= imageid;
}

这样就可以直接调用构造函数来实例化一个对象并赋值,代码相当简洁:

1
Plant peony = new Plant("牡丹","芍药科",6);

如果我们只需要给其中2个属性赋值,或者类中又增加新的属性,原来的构造函数可能不能再满足要求,我们需要提供新的构造函数重载。

现在有了对象初始化器,我们可以使用更简单的语法来实例化对象并赋值:

1
2
3
4
5
6
Plant peony = new Plant
{
    Name = "牡丹",
    Category="芍药科",
    ImageId= 6
}

我们可以根据需求随意增加或减少对属性的赋值。

接着来看看集合初始化器,习惯了对象初始化的语法,集合初始化器是水到渠成的:

1
2
3
4
5
List< Plant > plants = new List< Plant > {
    new Plant { Name = "牡丹", Category = "芍药科", ImageId =6},
    new Plant { Name = "莲", Category = "莲科", ImageId =10 },
    new Plant { Name = "柳", Category = "杨柳科", ImageId = 12 }
};

另一个常用的小伙伴Dictionary<K,V>类的对象也可以用类似的方式实例化:

1
2
3
4
5
6
Dictionary<int, Plant > plants = new Dictionary<int, Plant>
{
    { 11, new Plant { Name = "牡丹", Category = "芍药科", ImageId =6},
    { 12, new Plant { Name = "莲", Category = "莲科", ImageId =10 },
    { 13, new Plant { Name = "柳", Category = "杨柳科", ImageId = 12 }
};

使用对象初始化器或集合初始化器时赋值部分调用构造函数的圆括号可以省略,直接以花括号开始属性赋值即可。

在下文介绍匿名类和隐式类型数组时还会看到对象初始化器和集合初始化器的语法。

注意:对于C#3.0的新特性基本上都可以说是语法糖,因为运行的CLR没有变,只是编译器帮我们将简化的语法编译成我们之前需要手写的复杂的方式。

 

C++11 统一的初始化语法

C++11中统一了初始化对象的语法,这语法与C#的对象初始化器是孪生兄弟,就是一对花括号 &ndash; {}。我们由基本类型的初始化说起。

在C++11之前,我们初始化一个int一般写出这样:

1
int i(3);

1
int i = 3;

参见本小节末:初始化和赋值的区别

使用新的初始化语法可以写为:

1
int i{3};

同样char类型对象新的初始化方式:

1
char c{‘x‘};

使用赋值的方式下,下面代码是可以工作的:

1
int f=5.3;

赋值完成后f的值为5,编译器进行了窄转换,而使用新的初始化方式,窄转换就不会发生,即下面的代码无法通过编译:

1
int f{5.3};//注意,类型不匹配,无法通过编译

接着看一下类类型的例子:

我们使用与C#部分类型的类:

1
2
3
4
5
6
7
8
9
10
11
class Plant
{
  public:
    Plant();
    virtual ~Plant();
    string m_Name;
    string m_Category;
    unsigned int m_ImageId;
  protected:
  private:
};

不同于C#使用{}初始化类成员时需要显式指定类成员名称,C++类通过定义构造函数来获知初始化列表中参数的顺序。我们可以这样实现一个Plant类的构造函数,其中冒号开始的语法被称为"构造函数初始化列表":

1
2
3
4
Plant::Plant(string _name,string _category,unsigned int _imageId)
            :m_Name(_name),m_Category(_category),m_ImageId(_imageId)
{
}

别忘了在头文件中给新的构造函数重载加个声明,然后就可以这样实例化一个Plant对象了:

1
Plant plant{ "牡丹""芍药科", 6};

上面的例子都是在栈上分配的对象,对于堆上分配的对象,也可以使用new关键字加上新的初始化方式,如对于前面的Plant类,可以使用这种方式在堆上实例化一个新的对象:

1
Plant *plant = new Plant{ "牡丹""芍药科", 6};

 

对于struct,不需要实现重载构造就可以使用统一的初始化语法:

1
2
3
4
5
6
struct StPlant
{
    string m_Name;
    string m_Category;
    unsigned int m_ImageId;
};

可以直接这样实例化一个StPlant对象:

1
StPlant stplant{"牡丹""芍药科", 6};

在C++中声明,定义,初始化和赋值有着概念上的大不同,这对于用惯C#这样不太区分这种概念的语言的同学可能感觉很不理解。下面依次介绍下这几个概念:

声明,例如:

1
extern int i;

在类型名前添加一个extern关键字表示声明一个变量,这个变量在其他链接的文件中被定义。C++中一个变量可以被声明很多次但只能被定义一次。

定义:

1
int i;

定义是最常见的,注意,定义的同时也表示声明了这个变量。

初始化,初始化的方式有两种:

1
2
int i(5);
int i = 5;

前者是直接初始化,后者是拷贝初始化。这两者的不同是前者是寻找合适的拷贝/移动构造函数,后者是使用拷贝/移动赋值运算符。C++11后明确引入了右值及移动语意,初始化的性能大大提高。

赋值:

1
2
int i;
i=5;

这样把定义与赋值分开,则赋值的过程一定是调用拷贝/移动赋值运算符,而不是通过构造函数来完成。

最后看看下面这种写法:

1
extern int i = 5;

这样extern会被忽略,这是一个定义(含声明)及拷贝初始化变量的语句,且这个变量不能被再次定义。

C++11 初始化列表

标准库中的容器也可以使用统一的初始化方式进行填充:

1
std::vector<int> vec = {0, 1, 2, 3, 4};

更复杂一点的栗子:

1
2
3
4
5
vector<Plant> plants = {
    "牡丹""芍药科", 6},
    "牡丹""芍药科", 6},
    "牡丹""芍药科", 6}
};

同样std::map系列容器也可以使用类似的方式初始化:

1
2
3
4
5
map<int, Plant> plantsDic = {
    {1, { "牡丹""芍药科", 6}},
    {2, { "牡丹""芍药科", 6}},
    {3, { "牡丹""芍药科", 6}}
};

 

C++11对初始化列表支持的背后,一个其关键作用的角色就是新版标准库新增的std::initializer<T>模板类。编译器可以将{list}语法编译为std::initializer<T>类的对象。新版库中的容器也都添加了接收std::initializer<T>类型参数的构造函数重载,所以上面示例的几种写法都可以被支持。vector中增加的构造函数形如:

1
template <typename T> vector::vector(std::initializer_list<T> initList);

我们也可以在自己的函数实现中使用std::initializer<T>作为参数,如下代码:

注意:使用std::initializer<T>需要#include <initializer_list>

 

1
2
3
4
5
6
7
8
9
void GetGoodNum(std::initializer_list<double> marks) { 
 unsigned int num = 0; 
 // 统计80分以上学生人数 
 for_each (marks.begin(), marks.end(), [&num](double& m) { 
     if (m>80) { 
            num++; 
        
  }) 
}

这样我们就可以向函数传递一个{list}列表。

1
GetGoodNum({100,70.5,93,84,65});

这个例子用到了C++11的lambda表达式,后文有关于这个语法的介绍。

C# 隐式类型、匿名类和隐式类型数组

隐式类型

C#3.0中新增了var关键字。使用var关键字可以简化一些比较长,比较复杂不容易记忆的类型名的输入。不同于javascript中的var,C#中的var在编译之后会被替换为原有的类型,所以C#中var还是强类型的。

举几个简化我们输入的例子吧。Tuple是一个比较复杂的泛型类(下篇文章会有介绍),如果没有var,我们实例化一个Tuple对象的代码就像:

1
Tuple<string,string,int> plant = Tuple.Create("莲","莲科",1);

使用var代码就可以简化为:

1
var plant = Tuple.Create("莲","莲科",1);

在foreach循环中也常常是var的用武之地

1
2
foreach(var kvp in dictionaryObj)
{}

如果没有var,我们就要手写KeyValuePair<K,V>类型的名称,如果遍历的集合类是一个不常见的类型,诸如Enumerable.ToLookup()和Enumerable.GroupBy()方法返回的值,可能都记不清其中每一项的具体类型。使用var就可以轻松表示这一切。

var和后面要介绍的Linq也是结合最紧密的,一般Linq返回的都是一个类型非常复杂的对象。使用var能减少很大的编码工作量,使代码保持整洁。

 

匿名类

如果我们将前文介绍的对象初始化器语法中的new关键字类型去掉,这样就得到了匿名类,如:

1
2
3
4
5
6
var peony = new
{
    Name = "牡丹",
    Category="芍药科",
    ImageId= 6
}

匿名类中所有属性都是只读的,且其类型都是自动推导得来不能手动指定。当两个匿名类型具有相同的属性,则它们被认为是同一个匿名类型

如这个对象:

1
2
3
4
5
6
var peach = new
{
    Name = "桃花",
    Category = "蔷薇科",
    ImageId = 7,
};

判断类型的话,它们是相同的:

1
var sametype = peony.GetType() == peach.GetType();

匿名类型的属性可以直接用另一个对象的属性来初始化:

1
2
3
4
5
6
var football = new
{
    Name = "足球",
    Size = "Big",
    peach.ImageId,
};

这样football中就会有一个名为ImageId的属性,且值为7。当然也可以自定义名称,如果属性名相同省略就好。这种用法在LINQ的Select扩展方法中接收的lambda表达式创建新的匿名对象时常常会见到。

 

隐式类型数组

通过隐式类型数组这个特性,声明并初始化数组时也不用显式指定数组类型了。编译器会自动推导数组的类型,如:

一维数组

1
2
var a = new[] { 1, 10, 100, 1000 }; // int[]
var b = new[] { "hello"null"。world" }; // string[]

交错数组

1
2
3
4
5
var d = new[]
{
    new[]{"Luca""Mads""Luke""Dinesh"},
    new[]{"Karen""Suma""Frances"}
};

隐式类型数组也可以包含匿名类对象,当然所有的匿名类对象要符合同一个匿名类的定义。

参考下面这段示例代码:

 

1
2
3
4
5
6
7
8
9
10
11
var plants = new[] 
 new 
     Name = "莲"
     Categories= new[] { "山龙眼目""莲科","莲属" 
 }, 
 new 
     Name = " 柳"
     PhoneNumbers = new[] { "金虎尾目","杨柳科","柳属" 
 
};

 

C++11 类型推导

在C++中同样由于模板类型的大量使用导致某些类型的对象的类型不容易记忆及书写。C++11提供了auto关键字来解决这个问题。auto关键字的用法与C#中的var极为相似,即在需要指定具体类型的地方代之以auto关键字,如方法返回值前,以范围为基础的for循环中。

如:

1
2
3
string s("some lower case words");
for(auto it=s.begin(); it!=s.end && !isspace(*it); ++it);
*it=toupper(*it); //转换为大些字母

在C++11中还有一个更为强大的定义类型的操作符 - decltype。我们直接看一个例子,再来说明这个关键字的用法:

1
2
string s("some words");
decltype(s.size()) index=0;

代码中s.size()返回值的类型为string::size_type,decltype使用这个类型来定义index变量,代码中第二句相当于:

1
string::size_type index=0;

通过decltype可以简化很多类型的记忆及书写,编译器将在编译时自动以正确的类型替换。

关于decltype更详细的讨论,推荐学习C++ Primer(第5版)2.5.3节内容,其中讲述的decltype和引用的问题尤其值得认真学习。

 

C# 扩展方法

C#的扩展方法主要是为已存在,且不能或不方便直接修改的其代码的类添加方法。比如,C#3.0中为实现IEnumerable<T>接口的类型添加了如Where,Select等一些列扩展方法,从而可以以Fluent API的方法实现与LINQ等价的功能。这样,除了一些复杂的如join等通过LINQ语法实现更方便外,其他一些如简单的where通过Where扩展方法来完成则会使代码有更好的可读性。

怎样实现扩展方法?还是通过一个例子来介绍更直观:

在写代码时我们常遇到需要将一个集合以指定分隔符合并成一个字符串,即String.Join()方法完成的功能。一般的写法如下:

1
2
3
var list = new List<int>() {1,2,3,4,5};
list = list.Where(i=>i%2==0).ToList();
var str = string.Join(";", list);

可能你会想如果能在第二行代码一次生成字符串可能更方便,我们通过扩展IEnumerable<T>来实现这个功能:

1
2
3
4
5
6
7
public static class EnumerableExt
{
    public static string StrJoin<T>(this IEnumerable<T> enumerable, string spliter)
    {
        return string.Join(spliter, enumerable);
    }
}

可以看到扩展方法需要定义在静态类中,且扩展方法自身也需要是静态方法。扩展方法所在的类的名字不重要,相对而言这个类所在的命名空间的名字更重要,因为是通过引用的命名空间让编译器知道我们扩展方法来自于哪里。扩展方法最重要的部分为第一个参数,这个参数前面有一个this,表示我们要扩展这个参数的类型,扩展方法主要执行在这个参数对象上。除此之外实现扩展方法和实现一般方法相同。使用这个扩展方法重写之前的代码后:

1
2
var list = new List<int>() {1,2,3,4,5};
var str = list.Where(i=>i%2==0).StrJoin(",");

当然这个扩展方法不满足Fluent API传入参数和返回值类型相同的要求,但作为调用链最后一个方法未尝不可。

 

扩展方法这个特性C++没有类似功能,没得写。

 

C# Lambda表达式

在lambda表达式出现之前,只能通过委托表示一个函数,通过委托的实例或匿名函数来表示一个&ldquo;函数对象&rdquo;。有了lambda表达式,C#2.0中出现的匿名函数就可以退役了。lambda表达式可以完全取代匿名函数实现的功能。而且.NET Framework新增的Action及Func<T>系列委托类型也可以减少我们自定义委托类型的必要。

C#的lambda表达式的语法概括如下:

参数部分 => 方法体

对于参数部分,如果有2个或2个以上的参数需要用小括号括起来,lambda表达式的参数部分参数无需指定类型,编译器会自动进行类型推导。当然也可以明确指定参数类型:

1
(int x) => x+1;

对于方法体部分如果只有一条语句则无需加{},且对于有返回值的方法体也可以省略return关键字。如果是超过一条语句则需要{}且对于有返回值的情况不能省略return,如:

1
x => { x=x+1; return Math.Pow(x,2);}

C#中lambda表达式一般用于各种和委托类型相关的场景,比如一个方法接收委托类型参数或返回一个委托类型对象。在实现Fluent API样式的LINQ语法的那些扩展方法中很多都是接收委托类型的参数,如:

1
2
IEnumerable<TSource> Where<TSource>(this IEnumerable<TSource> source, Func<TSource, bool> predicate)
IEnumerable<TResult> Select<TSource, TResult>(this IEnumerable<TSource> source, Func<TSource, TResult> selector)

调用这些方法时,相应的参数传入lambda表达式就可。

关于闭包

闭包指的是在一

以上是关于C#与C++的发展历程第一 - 由C#3.0起的主要内容,如果未能解决你的问题,请参考以下文章

C#发展历程以及C#6.0新特性

C#发展历程以及C#6.0新特性

无法将 c# .Net Core 3.0 与 directx 9.0 依赖项链接

C#6.0新特性

一个C#开发者重温C++的心路历程

为啥 C++ CLI 索引属性在 C# 中不起作用?