序、扯淡
现在我做.net这块并不多,因为现在上班的公司不用.net。为了混口饭吃,就转去做Python开发了。
我一直觉得换一门主力开发语言并不是一件非常困难的事情,因为我认为编程语言只是思想的一种具体表达方式,用于构成一个程序的肉身,而一个程序里最重要的灵魂,是具体的编程思想、算法和设计模式组成的。
所以我也是这么和身边的人这么“传教”的,虽说语言本身也很重要,但算法设计模式等等千万别落下,各个语言自带的语法糖其实都可以用代码实现,万变不离其宗。
扯得有些远了,这次的题目主要是因为有个学弟,在我转到Python之后,也跟着转到Python来了。有次谈天的时候我表示不希望他到时候C#半桶水叮当不说,到时候Python也是这样。C#除了国内的就业前景相比之下并不是特别好以外,就语言本身素质来说还是非常不错的,用了这么久的Python,貌似只有Python里的列表切片和负索引(例如-1表示列表倒数第一位元素)以外,没有C#做不到的。
然后这个学弟就给我举反例了:
当时因为算是上班时间摸鱼,所以就只是去翻了一下System.Linq命名空间下面的拓展方法,发现没有什么类似的拓展方法后,也就以为C#里的确没有这种写法,后来就没再去注意。
最近两天又突然记起这件事情,仔细琢磨了一下,发现自己当时怕是智障了:都想到去翻System.Linq命名空间了,怎么就想不起直接用LINQ呢?
一、概念讲解
推导式是Python中一个非常有用的类似于语法糖的存在,比如有下面一个名为numbers的列表(就是一个0-15的列表):
1 2 3 4 5 6 |
numbers = [ 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F ] |
如果我需要将整个列表中的数字翻倍,可以这么写:
1 |
numbers = [number * 2 for number in numbers] |
或者我只要列表中的奇数:
1 |
numbers = [number for number in numbers if number % 2 == 1] |
除此以外还有字典推导式和生成器表达式:
有这么一个字典:
1 2 3 4 5 6 7 |
d = { 'key1': 'value1', 'key2': 'value2', 'key3': 'value3', 'key4': 'value4', 'key5': 'value5' } |
现在需要将其键值对进行翻转,可以直接使用字典推导式创建一个新字典:
1 |
{value: key for key, value in d.items()} |
至于生成器表达式,我个人对其的理解是和列表推导式结构差不多,但列表推导式是以中括号包裹,生成器表达式则是以小括号包裹,且生成器表达式返回的是一个生成器,在迭代等需要使用其值时,才会在内存中计算产生,在源数据来自非常大的列表时,使用生成器表达式可以节约内存。
可以看到列表推导式、字典推导式和生成器表达式这种写法可以在对可迭代对象进行处理时省去一个for循环结构。使得代码更加简洁。
二、C#的独门绝技——LINQ
LINQ(Language Integrated Query,语言集成查询)是.net Framework在3.5版本中新增的一项新功能,它允许编写C#或者Visual Basic代码以查询数据库相同的方式操作内存数据。
是的,我们在C#里想要像Python那样用列表推导,可以依靠LINQ实现。
列表推导式:
还是以第一章中的数据和用法为例:
先定义numbers数组,LINQ支持从所有继承IEnumerable<T>接口的对象中获取数据:
1 2 3 4 5 6 |
int[] numbers = new int[] { 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F }; |
对整个数组中的元素进行翻倍:
1 |
numbers = (from number in numbers select number * 2).ToArray(); |
剔除数组中的偶数:
1 |
numbers = (from number in numbers where number % 2 == 1 select number).ToArray(); |
字典推导式:
不过要用LINQ实现Python的字典就比较麻烦了,因为Python中的字典对象在很多情况下表现的更像一个C#和Java里的数据模型类(话说当前最新的Python 3.7也将引入dataclass了),但如果一定要做,也是可以做到的。
假设现在有这么一个字典对象dict:
1 2 3 4 5 6 7 8 |
Dictionary<string, string> dict = new Dictionary<string, string>() { { "key1", "value1"}, { "key2", "value2"}, { "key3", "value3"}, { "key4", "value4"}, { "key5", "value5"}, }; |
现在将其键值对反转:
1 |
dict = (from pair in dict select new { Key = pair.Value, Value = pair.Key }).ToDictionary(item => item.Key, item => item.Value); |
不过这段代码在这里会产生一个匿名类,看着不是特别爽。不知道有没有别的好方法,欢迎各位看官留言指点。
后续更新:
写的时候只想着要用LINQ了,忘了可以直接对字典对象使用System.Linq命名空间里的ToDictionary()方法:
1 |
dict = dict.ToDictionary(item => item.Value, item => item.Key); |
生成器表达式:
其实这个生成器表达式LINQ本身就有这个功能,因为LINQ的特性中就有一个延迟加载。什么是延迟加载?说的简单点就是需要时再去获取,不需要的时候就什么都不做。
在刚刚上面的LINQ实现列表推导式和字典推导式的段落里,笔者在最后使用了ToArray()和ToDictionary()两个方法,将结果直接转换成了对应的类型。如果在LINQ语句后面不写类似的ToXXXX()方法的话,LINQ语句将返回一个IEnumerable<T>的对象实例,但此时并没有去计算产生新的值。只有当调用ToXXXX()方法或者用foreach语句去迭代时,才会去计算。
您说有什么简单的方法能够证明LINQ用的是延迟加载?
我们可以看一下这段代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
static void Main(string[] args) { int[] numbers = new int[] { 0x00, 0x01, 0x02, 0x03, }; IEnumerable<int> iEnumerable = from number in numbers select number * 2; int index = 0; numbers[0] = 0xFF; foreach (int number in iEnumerable) { Console.WriteLine($"{index++} - {number}"); } Console.ReadKey(); } |
我们在调用LINQ语句后,手动修改了numbers数组的第一位元素,让我们看一下输出:
可以看到输出的数据里,第一位的元素并不是2,而是510,在LINQ语句后面的值修改代码确确实实的影响到最后的输出,证明了LINQ方法本身是带有延迟加载特性的。
三、写在最后
其实本篇文章并不是想证明C#有多么多么好,可以秒Python之类的语言balabala…,笔者只是单纯的想再提一下LINQ,这项作为.net Framework 3.5的新亮点引入的技术还有各种各样非常有意思的使用方式值得去挖掘。
后面如果有空,我会再来聊聊它的。
小柊
2018年5月1日 21:18:19