EVAL 命令

Redis Eval 命令使用 Lua 解释器执行脚本。

1
redis 127.0.0.1:6379> EVAL script numkeys key [key ...] arg [arg ...] 
  • EVAL 命令的关键字。
  • luascript Lua 脚本。
  • numkeys 指定的 Lua 脚本需要处理键的数量,其实就是 key数组的长度。
  • key 传递给 Lua 脚本零到多个键,空格隔开,在 Lua 脚本中通过 KEYS[INDEX]来获取对应的值,其中1 <= INDEX <= numkeys。
  • arg是传递给脚本的零到多个附加参数,空格隔开,在 Lua 脚本中通过ARGV[INDEX]来获取对应的值,其中1 <= INDEX <= numkeys。

注意:numkeys无论什么情况下都是必须的命令参数。

1
2
3
4
5
6
7
8
127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> get hello
"world"
127.0.0.1:6379> EVAL "return redis.call('GET',KEYS[1])" 1 hello
"world"
127.0.0.1:6379> EVAL "return redis.call('GET','hello')" 0
"world"

call 函数和 pcall 函数

区别:
call 执行命令错误时会向调用者直接返回一个错误;
pcall 则会将错误包装为一个我们上面讲的 table 表。

1
2
3
4
5
127.0.0.1:6379> EVAL "return redis.call('no_command')" 0
(error) ERR Error running script (call to f_1e6efd00ab50dd564a9f13e5775e27b966c2141e): @user_script:1: @user_script: 1: Unknown Redis command called from Lua script
127.0.0.1:6379> EVAL "return redis.pcall('no_command')" 0
(error) @user_script: 1: Unknown Redis command called from Lua script
127.0.0.1:6379>

值转换

Lua 脚本向 Redis 返回小数,那么会损失小数精度,转换为字符串则是安全的。

1
2
3
4
5
127.0.0.1:6379> EVAL "return 3.14" 0
(integer) 3
127.0.0.1:6379> EVAL "return tostring(3.14)" 0
"3.14"
127.0.0.1:6379>

原子执行

Lua 脚本在 Redis 中是以原子方式执行的,在 Redis 服务器执行EVAL命令时,在命令执行完毕并向调用者返回结果之前,只会执行当前命令指定的 Lua 脚本包含的所有逻辑,其它客户端发送的命令将被阻塞,直到EVAL命令执行完毕为止。因此 LUA 脚本不宜编写一些过于复杂了逻辑,必须尽量保证 Lua 脚本的效率,否则会影响其它客户端。

脚本管理

SCRIPT LOAD

加载脚本到缓存以达到重复使用,避免多次加载浪费带宽,每一个脚本都会通过 SHA 校验返回唯一字符串标识。需要配合 EVALSHA 命令来执行缓存后的脚本。

1
2
3
4
127.0.0.1:6379> SCRIPT LOAD "return 'hello'"
"1b936e3fe509bcbc9cd0664897bbe8fd0cac101b"
127.0.0.1:6379> EVALSHA 1b936e3fe509bcbc9cd0664897bbe8fd0cac101b 0
"hello"

SCRIPT FLUSH

清除所有的脚本缓存。Redis并没有根据 SHA 来删除脚本缓存,所以在生产中一般不会再生产过程中使用该命令。

SCRIPT EXISTS

以 SHA 标识为参数检查一个或者多个缓存是否存在。

1
2
3
127.0.0.1:6379> SCRIPT EXISTS 1b936e3fe509bcbc9cd0664897bbe8fd0cac101b
1) (integer) 1
127.0.0.1:6379>

SCRIPT KILL

终止正在执行的脚本。但是为了数据的完整性此命令并不能保证一定能终止成功。如果当一个脚本执行了一部分写的逻辑而需要被终止时,该命令是不凑效的。需要执行SHUTDOWN nosave在不对数据执行持久化的情况下终止服务器来完成终止脚本。

总结

  • 务必对 Lua 脚本进行全面测试以保证其逻辑的健壮性,当 Lua 脚本遇到异常时,已经执行过的逻辑是不会回滚的。
  • 尽量不使用 Lua 提供的具有随机性的函数,参见相关官方文档。
  • 在 Lua 脚本中不要编写 function 函数,整个脚本作为一个函数的函数体。
  • 在脚本编写中声明的变量全部使用 local 关键字。
  • 在集群中使用 Lua 脚本要确保逻辑中所有的 key分 到相同机器,也就是同一个插槽 (slot) 中,可采用 Redis Hash Tag 技术。
  • Lua 脚本一定不要包含过于耗时、过于复杂的逻辑。

参考:

一网打尽Redis Lua脚本并发原子组合操作

EVAL Script

Redis Eval 命令

简介

Lua 是一种轻量小巧的脚本语言,用标准C语言编写并以源代码形式开放, 其设计目的是为了嵌入应用程序中,从而为应用程序提供灵活的扩展和定制功能。

Lua 特性

  • 轻量级: 它用标准C语言编写并以源代码形式开放,编译后仅仅一百余K,可以很方便的嵌入别的程序里。
  • 可扩展: Lua提供了非常易于使用的扩展接口和机制:由宿主语言(通常是C或C++)提供这些功能,Lua可以使用它们,就像是本来就内置的功能一样。
  • 其它特性:
    支持面向过程(procedure-oriented)编程和函数式编程(functional programming);
    自动内存管理;只提供了一种通用类型的表(table),用它可以实现数组,哈希表,集合,对象;
    语言内置模式匹配;闭包(closure);函数也可以看做一个值;提供多线程(协同进程,并非操作系统所支持的线程)支持;
    通过闭包和table可以很方便地支持面向对象编程所需要的一些关键机制,比如数据抽象,虚函数,继承和重载等。

Lua 应用场景

  • 游戏开发
  • 独立应用脚本
  • Web 应用脚本
  • 扩展和数据库插件如:MySQL Proxy 和 MySQL WorkBench
  • 安全系统,如入侵检测系统

Lua 环境安装

参考:

Lua 环境安装

Lua 基本语法

交互式编程

1
2
3
4
5
6
# 启用
lua -i 或 lua

#命令行输入命令
print("Hello World!")
Hello World!# 输出

脚本式编程

将 Lua 程序代码保存到一个以 lua 结尾的文件,并执行,该模式称为脚本式编程

hello.lua

1
2
print("Hello World!")
print("test1")

执行:

1
$ lua hello.lua

也可以将代码修改为如下形式来执行脚本(在开头添加:#!/usr/local/bin/lua),指定了 Lua 的解释器 /usr/local/bin directory。加上 # 号标记解释器会忽略它。接下来我们为脚本添加可执行权限,并执行:

1
2
3
4
#!/usr/local/bin/lua

print("Hello World!")
print("www.runoob.com")

执行:

1
./hello.lua

注释

单行注释:

两个减号是单行注释: –

多行注释:

1
2
3
4
--[[
多行注释
多行注释
--]]

多行注释推荐使用 –[=[注释内容]=],这样可以避免遇到 table[table[idx]] 时就将多行注释结束了。

注意:多行注释加 - 取消注释中间代码可以继续运行,单行注释没有此功能。

标示符

标示符以一个字母 A 到 Z 或 a 到 z 或下划线 _ 开头后加上 0 个或多个字母,下划线,数字(0 到 9)。

最好不要使用下划线加大写字母的标示符,因为Lua的保留字也是这样的。

Lua 不允许使用特殊字符如 @, $, 和 % 来定义标示符。 Lua 是一个区分大小写的编程语言。

关键词

1
2
3
4
5
6
and	break	do	else
elseif end false for
function if in local
nil not or repeat
return then true until
while goto

一般约定,以下划线开头连接一串大写字母的名字(比如 _VERSION)被保留用于 Lua 内部全局变量。

全局变量

在默认情况下,变量总是认为是全局的。

全局变量不需要声明,给一个变量赋值后即创建了这个全局变量,访问一个没有初始化的全局变量也不会出错,只不过得到的结果是:nil。

如果你想删除一个全局变量,只需要将变量赋值为nil。当且仅当一个变量不等于nil时,这个变量即存在。

1
2
3
4
5
print(b)
nil
> b=10
> print(b)
10

数据类型

Lua 是动态类型语言,变量不要类型定义,只需要为变量赋值。 值可以存储在变量中,作为参数传递或结果返回。

Lua 中有 8 个基本类型分别为:nil、boolean、number、string、userdata、function、thread 和 table。

微信截图_20201019145429.png

1
2
3
4
5
6
7
print(type("Hello world"))      --> string
print(type(10.4*3)) --> number
print(type(print)) --> function
print(type(type)) --> function
print(type(true)) --> boolean
print(type(nil)) --> nil
print(type(type(X))) --> string

nil(空)

1
2
> print(type(a))
nil

nil 作比较时应该加上双引号 “:

1
2
3
4
5
6
7
8
9
10
> type(X)
nil
> type(X)==nil
false
> type(X)=="nil"
true

-- 没有使用type(变量)这个形式,就不需要加引号
print(x == nil) --结果为true
print(x == "nil")--结果为false

type(X)==nil 结果为 false 的原因是因为 type(type(X))==string。

boolean(布尔)

boolean 类型只有两个可选值:true(真) 和 false(假),Lua 把 false 和 nil 看作是 false,其他的都为 true,数字 0 也是 true:

number(数字)

Lua 默认只有一种 number 类型 – double(双精度)类型(默认类型可以修改 luaconf.h 里的定义),以下几种写法都被看作是 number 类型:

1
2
3
4
5
6
print(type(2))
print(type(2.2))
print(type(0.2))
print(type(2e+1))
print(type(0.2e-1))
print(type(7.8263692594256e-06))

string(字符串)

字符串由一对双引号或单引号来表示。

1
2
string1 = "this is string1"
string2 = 'this is string2'

也可以用 2 个方括号 “[[]]” 来表示”一块”字符串。

1
2
3
4
5
6
7
8
9
html = [[
<html>
<head></head>
<body>
<a href="http://www.runoob.com/">菜鸟教程</a>
</body>
</html>
]]
print(html)

对一个数字字符串上进行算术操作时,Lua 会尝试将这个数字字符串转成一个数字:

1
2
3
4
5
6
7
8
> print("2" + 6)
8.0
> print("2" + "6")
8.0
> print("2 + 6")
2 + 6
> print("-2e2" * "6")
-1200.0

使用 # 来计算字符串的长度,放在字符串前面,如下实例:

1
2
3
4
5
> len = "www.runoob.com"
> print(#len)
14
> print(#"www.runoob.com")
14

运行时,Lua会自动在string和numbers之间自动进行类型转换,当一个字符串使用算术操作符时, string 就会被转成数字。反过来,当 Lua 期望一个 string 而碰到数字时,会将数字转成 string。

table(表)

在 Lua 里,table 的创建是通过”构造表达式”来完成,最简单构造表达式是{},用来创建一个空表。也可以在表里添加一些数据,直接初始化表:

1
2
3
4
5
-- 创建一个空的 table
local tbl1 = {}

-- 直接初始表
local tbl2 = {"apple", "pear", "orange", "grape"}

Lua 中的表(table)其实是一个”关联数组”(associative arrays),数组的索引可以是数字或者是字符串。

1
2
3
4
5
6
7
8
9
-- table_test.lua 脚本文件
a = {}
a["key"] = "value"
key = 10
a[key] = 22
a[key] = a[key] + 11
for k, v in pairs(a) do
print(k .. " : " .. v)
end

脚本执行结果为:

1
2
3
$ lua table_test.lua 
key : value
10 : 33

不同于其他语言的数组把 0 作为数组的初始索引,在 Lua 里表的默认初始索引一般以 1 开始。

1
2
3
4
5
-- table_test2.lua 脚本文件
local tbl = {"apple", "pear", "orange", "grape"}
for key, val in pairs(tbl) do
print("Key", key)
end

脚本执行结果为:

1
2
3
4
5
$ lua table_test2.lua 
Key 1
Key 2
Key 3
Key 4

table 不会固定长度大小,有新数据添加时 table 长度会自动增长,没初始的 table 都是 nil。

1
2
3
4
5
6
7
8
-- table_test3.lua 脚本文件
a3 = {}
for i = 1, 10 do
a3[i] = i
end
a3["key"] = "val"
print(a3["key"])
print(a3["none"])

脚本执行结果为:

1
2
3
$ lua table_test3.lua 
val
nil

function(函数)

在 Lua 中,函数是被看作是”第一类值(First-Class Value)”,函数可以存在变量里:

1
2
3
4
5
6
7
8
9
10
11
-- function_test.lua 脚本文件
function factorial1(n)
if n == 0 then
return 1
else
return n * factorial1(n - 1)
end
end
print(factorial1(5))
factorial2 = factorial1
print(factorial2(5))

脚本执行结果为:

1
2
3
$ lua function_test.lua 
120
120

function 可以以匿名函数(anonymous function)的方式通过参数传递:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- function_test2.lua 脚本文件
function testFun(tab,fun)
for k ,v in pairs(tab) do
print(fun(k,v));
end
end


tab={key1="val1",key2="val2"};
testFun(tab,
function(key,val)--匿名函数
return key.."="..val;
end
);

脚本执行结果为:

1
2
3
$ lua function_test2.lua 
key1 = val1
key2 = val2

thread(线程)

在 Lua 里,最主要的线程是协同程序(coroutine)。它跟线程(thread)差不多,拥有自己独立的栈、局部变量和指令指针,可以跟其他协同程序共享全局变量和其他大部分东西。

线程跟协程的区别:线程可以同时多个运行,而协程任意时刻只能运行一个,并且处于运行状态的协程只有被挂起(suspend)时才会暂停。

userdata(自定义类型)

userdata 是一种用户自定义数据,用于表示一种由应用程序或 C/C++ 语言库所创建的类型,可以将任意 C/C++ 的任意数据类型的数据(通常是 struct 和 指针)存储到 Lua 变量中调用。

Lua 变量

变量在使用前,需要在代码中进行声明,即创建该变量。

编译程序执行代码之前编译器需要知道如何给语句变量开辟存储区,用于存储变量的值。

Lua 变量有三种类型:全局变量、局部变量、表中的域。

Lua 中的变量全是全局变量,那怕是语句块或是函数里,除非用 local 显式声明为局部变量。

局部变量的作用域为从声明位置开始到所在语句块结束。

变量的默认值均为 nil。

尽可能的使用局部变量,有两个好处:

  • 避免命名冲突。
  • 访问局部变量的速度比全局变量更快。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
-- test.lua 文件脚本
a = 5 -- 全局变量
local b = 5 -- 局部变量

function joke()
c = 5 -- 全局变量
local d = 6 -- 局部变量
end

joke()
print(c,d) --> 5 nil

do
local a = 6 -- 局部变量
b = 6 -- 对局部变量重新赋值
print(a,b); --> 6 6
end

print(a,b) --> 5 6

赋值语句

1
2
a = "hello" .. "world"
t.n = t.n + 1

Lua 可以对多个变量同时赋值,变量列表和值列表的各个元素用逗号分开,赋值语句右边的值会依次赋给左边的变量。

1
a, b = 10, 2*x       <-->       a=10; b=2*x

遇到赋值语句Lua会先计算右边所有的值然后再执行赋值操作,所以我们可以这样进行交换变量的值:

1
2
x, y = y, x                     -- swap 'x' for 'y'
a[i], a[j] = a[j], a[i] -- swap 'a[i]' for 'a[j]'

当变量个数和值的个数不一致时,Lua会一直以变量个数为基础采取以下策略:

1
2
a. 变量个数 > 值的个数             按变量个数补足nil
b. 变量个数 < 值的个数 多余的值会被忽略
1
2
3
4
5
6
7
8
a, b, c = 0, 1
print(a,b,c) --> 0 1 nil

a, b = a+1, b+1, b+2 -- value of b+2 is ignored
print(a,b) --> 1 2

a, b, c = 0
print(a,b,c) --> 0 nil nil

Lua 对多个变量同时赋值,不会进行变量传递,仅做值传递:

1
2
3
4
5
6
7
8
9
10
a, b = 0, 1
a, b = a+1, a+1
print(a,b) --> 1 1
a, b = 0, 1
a, b = b+1, b+1
print(a,b) --> 2 2
a, b = 0, 1
a = a+1
b = a+1
print(a,b) --> 1 2

索引

对 table 的索引使用方括号 []。Lua 也提供了 . 操作。

1
2
3
t[i]
t.i -- 当索引为字符串类型时的一种简化写法
gettable_event(t,i) -- 采用索引访问本质上是一个类似这样的函数调用
1
2
3
4
5
6
> site = {}
> site["key"] = "www.runoob.com"
> print(site["key"])
www.runoob.com
> print(site.key)
www.runoob.com

循环

while 循环

while 循环语法:

1
2
3
4
while(condition)
do
statements
end

for 循环

for语句有两大类::

数值for循环

泛型for循环

数值for循环

1
2
3
for var=exp1,exp2,exp3 do  
<执行体>
end

var 从 exp1 变化到 exp2,每次变化以 exp3 为步长递增 var,并执行一次 “执行体”。exp3 是可选的,如果不指定,默认为1。

for的三个表达式在循环开始前一次性求值,以后不再进行求值。比如上面的f(x)只会在循环开始前执行一次,其结果用在后面的循环中。

1
2
3
4
5
6
7
#!/usr/local/bin/lua  
function f(x)
print("function")
return x*2
end
for i=1,f(5) do print(i)
end

输出:

1
2
3
4
5
6
7
8
9
10
11
function
1
2
3
4
5
6
7
8
9
10

泛型for循环

泛型 for 循环通过一个迭代器函数来遍历所有值,类似 java 中的 foreach 语句。迭代的下标是从1开始。

Lua 编程语言中泛型 for 循环语法格式:

1
2
3
4
5
--打印数组a的所有值  
a = {"one", "two", "three"}
for i, v in ipairs(a) do
print(i, v)
end

i是数组索引值,v是对应索引的数组元素值。ipairs是Lua提供的一个迭代器函数,用来迭代数组。

for 循环中,循环的索引 i 为外部索引,修改循环语句中的内部索引 i,不会影响循环次数:

1
2
3
4
for i=1,10 do
i = 10
print("one time,i:"..i)
end

仍然循环 10 次,只是 i 的值被修改了。

在 lua 中 pairs 与 ipairs 两个迭代器的用法相近,但有一点是不一样的:

pairs 能迭代所有键值对。

ipairs 可以想象成 int+pairs,只会迭代键为数字的键值对,只能遍历所有数组下标的值。
如果 ipairs 在迭代过程中是会直接跳过所有手动设定key值的变量。

pairs 可以遍历表中所有的 key,并且除了迭代器本身以及遍历表本身还可以返回 nil;

但是 ipairs 则不能返回nil,只能返回数字0,如果遇到nil则退出。它只能遍历到表中出现的第一个不是整数的key。

例如:

1
2
3
4
tab = {1,2,a= nil,"d"}
for i,v in ipairs(tab) do
print(i,v)
end

输出结果为:

1
2
3
1  1
2 2
3 d

这里是直接跳过了 a=nil 这个变量

第二,ipairs 在迭代过程中如果遇到nil时会直接停止。

1
2
3
4
tab = {1,2,a= nil,nil,"d"}
for i,v in ipairs(tab) do
print(i,v)
end

输出结果为:

1
2
1  1
2 2

这里会在遇到 nil 的时候直接跳出循环。

repeat…until 循环

repeat…until 循环的条件语句在当前循环结束后判断。

1
2
3
repeat
statements
until( condition )
1
2
3
4
5
6
7
--[ 变量定义 --]
a = 10
--[ 执行循环 --]
repeat
print("a的值为:", a)
a = a + 1
until( a > 15 )

输出:

1
2
3
4
5
6
a的值为:    10
a的值为: 11
a的值为: 12
a的值为: 13
a的值为: 14
a的值为: 15

循环控制语句

break 语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
--[ 定义变量 --]
a = 10

--[ while 循环 --]
while( a < 20 )
do
print("a 的值为:", a)
a=a+1
if( a > 15)
then
--[ 使用 break 语句终止循环 --]
break
end
end

goto 语句

Lua 语言中的 goto 语句允许将控制流程无条件地转到被标记的语句处。

语法格式如下所示:

1
goto Label

Label 的格式为:

1
:: Label ::

以下实例在判断语句中使用 goto:

1
2
3
4
5
6
7
local a = 1
::label:: print("--- goto label ---")

a = a+1
if a < 3 then
goto label -- a 小于 3 的时候跳转到标签 label
end

输出结果为:

1
2
--- goto label ---
--- goto label ---

从输出结果可以看出,多输出了一次 — goto label —。

以下实例演示了可以在 lable 中设置多个语句:

1
2
3
4
5
6
7
8
9
i = 0
::s1:: do
print(i)
i = i+1
end
if i>3 then
os.exit() -- i 大于 3 时退出
end
goto s1

输出结果为:

1
2
3
4
0
1
2
3

continue功能

1
2
3
4
5
6
7
8
9
for i=1, 3 do
if i <= 2 then
print(i, "yes continue")
goto continue
end
print(i, " no continue")
::continue::
print([[i'm end]])
end

输出:

1
2
3
4
5
6
1   yes continue
i'm end
2 yes continue
i'm end
3 no continue
i'm end

流程控制

if 语句

1
2
3
4
5
6
7
8
9
10
--[ 定义变量 --]
a = 10;

--[ 使用 if 语句 --]
if( a < 20 )
then
--[ if 条件为 true 时打印以下信息 --]
print("a 小于 20" );
end
print("a 的值为:", a);

if…else 语句

1
2
3
4
5
6
7
8
9
10
11
12
--[ 定义变量 --]
a = 100;
--[ 检查条件 --]
if( a < 20 )
then
--[ if 条件为 true 时执行该语句块 --]
print("a 小于 20" )
else
--[ if 条件为 false 时执行该语句块 --]
print("a 大于 20" )
end
print("a 的值为 :", a)

if…elseif…else 语句

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
--[ 定义变量 --]
a = 100

--[ 检查布尔条件 --]
if( a == 10 )
then
--[ 如果条件为 true 打印以下信息 --]
print("a 的值为 10" )
elseif( a == 20 )
then
--[ if else if 条件为 true 时打印以下信息 --]
print("a 的值为 20" )
elseif( a == 30 )
then
--[ if else if condition 条件为 true 时打印以下信息 --]
print("a 的值为 30" )
else
--[ 以上条件语句没有一个为 true 时打印以下信息 --]
print("没有匹配 a 的值" )
end
print("a 的真实值为: ", a )

函数

在Lua中,函数是对语句和表达式进行抽象的主要方法。既可以用来处理一些特殊的工作,也可以用来计算一些值。

Lua 提供了许多的内建函数,你可以很方便的在程序中调用它们,如 print() 函数可以将传入的参数打印在控制台上。

Lua 函数主要有两种用途:

  • 1.完成指定的任务,这种情况下函数作为调用语句使用;
  • 2.计算并返回值,这种情况下函数作为赋值语句的表达式使用。

函数定义

1
2
3
4
optional_function_scope function function_name( argument1, argument2, argument3..., argumentn)
function_body
return result_params_comma_separated
end

解析:

  • optional_function_scope: 该参数是可选的制定函数是全局函数还是局部函数,未设置该参数默认为全局函数,如果你需要设置函数为局部函数需要使用关键字 local。

  • function_name: 指定函数名称。

  • argument1, argument2, argument3…, argumentn: 函数参数,多个参数以逗号隔开,函数也可以不带参数。

  • function_body: 函数体,函数中需要执行的代码语句块。

  • result_params_comma_separated: 函数返回值,Lua语言函数可以返回多个值,每个值以逗号隔开。

将函数作为参数传递给函数,如下实例:

1
2
3
4
5
6
7
8
9
10
11
12
myprint = function(param)
print("这是打印函数 - ##",param,"##")
end

function add(num1,num2,functionPrint)
result = num1 + num2
-- 调用传递的函数参数
functionPrint(result)
end
myprint(10)
-- myprint 函数作为参数传递
add(2,5,myprint)

以上代码执行结果为:

1
2
这是打印函数 -   ##    10    ##
这是打印函数 - ## 7 ##

多返回值

Lua函数可以返回多个结果值,比如string.find,其返回匹配串”开始和结束的下标”(如果不存在匹配串返回nil)。

1
2
3
> s, e = string.find("www.runoob.com", "runoob") 
> print(s, e)
5 10

Lua函数中,在return后列出要返回的值的列表即可返回多值,如:

1
2
3
4
5
6
7
8
9
10
11
12
13
function maximum (a)
local mi = 1 -- 最大值索引
local m = a[mi] -- 最大值
for i,val in ipairs(a) do
if val > m then
mi = i
m = val
end
end
return m, mi
end

print(maximum({8,10,23,12,5}))

以上代码执行结果为:

1
23    3

可变参数

Lua 函数可以接受可变数目的参数,和 C 语言类似,在函数参数列表中使用三点 … 表示函数有可变的参数。

1
2
3
4
5
6
7
8
function add(...)  
local s = 0
for i, v in ipairs{...} do --> {...} 表示一个由所有变长参数构成的数组
s = s + v
end
return s
end
print(add(3,4,5,6,7)) --->25

通过 select("#",...) 来获取可变参数的数量:

1
2
3
4
5
6
7
8
9
10
11
function average(...)
result = 0
local arg={...}
for i,v in ipairs(arg) do
result = result + v
end
print("总共传入 " .. select("#",...) .. " 个数")
return result/select("#",...)
end

print("平均值为",average(10,5,3,4,5,6))

以上代码执行结果为:

1
2
总共传入 6 个数
平均值为 5.5

如果是几个固定参数加上可变参数,固定参数必须放在变长参数之前

1
2
3
4
5
6
function fwrite(fmt, ...)  ---> 固定的参数fmt
return io.write(string.format(fmt, ...))
end

fwrite("runoob\n") --->fmt = "runoob", 没有变长参数。
fwrite("%d%d\n", 1, 2) --->fmt = "%d%d", 变长参数为 1 和 2

通常在遍历变长参数的时候只需要使用 {…},然而变长参数可能会包含一些 nil,那么就可以用 select 函数来访问变长参数了:select(‘#’, …) 或者 select(n, …)

  • select(‘#’, …) 返回可变参数的长度
  • select(n, …) 用于返回 n 到 select(‘#’,…) 的参数

调用 select 时,必须传入一个固定实参 selector(选择开关)和一系列变长参数。如果selector 为数字n,那么 select 返回它的第n个可变实参,否则只能为字符串”#”,这样 select 会返回变长参数的总数。例子代码:

1
2
3
4
5
6
7
8
9
10
do  
function foo(...)
for i = 1, select('#', ...) do -->获取参数总数
local arg = select(i, ...); -->读取参数
print("arg", arg);
end
end

foo(1, 2, 3, 4);
end

输出结果为:

1
2
3
4
arg    1
arg 2
arg 3
arg 4
1
2
3
4
5
6
7
8
select(n,...) --> 返回的是多个参数,而不是一个table
arg = select(n,...) --> 只是接收了第一个参数,参考<多返回值>部分

-- 验证代码
arg = select(2, 1, 6, 8, 8, 9) --> 相当于arg = 6, 8, 8, 9
print(arg) --> 输出为:6
print(type(arg)) --> 输出为:number(验证了是多返回值的第一个number,而不是打印arg第一个参数的类型table)
print(select(2, 1, 6, 8, 8, 9)) --> 输出为:6 8 8 9

注意:多返回值的函数在赋值时的情况,仅仅只有放在所有逗号之后的那个函数会把返回值展开。

这里列举一个典型情况:

1
2
3
4
5
6
7
8
9
10
function add()
return 1,0
end

local b,c,d,e = add(),add()

print(b) -- 1
print(c) -- 1
print(d) -- 0
print(e) -- nil

运算符

算术运算符

微信截图_20201019163051.png

关系运算符

微信截图_20201019163124.png

逻辑运算符

微信截图_20201019163151.png

其他运算符

微信截图_20201019163220.png

1
2
3
4
5
6
7
8
9
10
a = "Hello "
b = "World"

print("连接字符串 a 和 b ", a..b )

print("b 字符串长度 ",#b )

print("字符串 Test 长度 ",#"Test" )

print("菜鸟教程网址长度 ",#"www.runoob.com" )

输出:

1
2
3
4
连接字符串 a 和 b     Hello World
b 字符串长度 5
字符串 Test 长度 4
菜鸟教程网址长度 14

# 获取表的最大索引的值。

1
2
3
4
5
6
7
8
9
tab1 = {"1","2"}
print("tab1长度"..#tab1)
tab2 = {key1="1","2"}
print("tab2长度"..#tab2)
tab3 = {}
tab3[1]="1"
tab3[2]="2"
tab3[4]="4"
print("tab3长度"..#tab3)

输出:

1
2
3
tab1长度2
tab2长度1
tab3长度4

下标越过 1 位以上,长度还是为 2:

1
2
3
4
5
tab3={}
tab3[1]="1"
tab3[2]="2"
tab3[5]="5"
print("tab3的长度",#tab3)

输出:

1
tab3的长度    2

三元表达式

利用 and 和 or 的特性,若(A and B) A 为 false 返回 A,(A or B)A 为 false 返回 B,以及除 nil 外其他数据类型被当做 true。

可以非常简单的完成:

1
2
local isAppel = false
print(isAppel and "苹果" or "梨")

运算符优先级

从高到低的顺序:

1
2
3
4
5
6
7
8
^
not - (unary)
* / %
+ -
..
< > <= >= ~= ==
and
or

除了 ^ 和 .. 外所有的二元运算符都是左连接的。

1
2
3
4
5
a+i < b/2+1          <-->       (a+i) < ((b/2)+1)
5+x^2*8 <--> 5+((x^2)*8)
a < y and y <= z <--> (a < y) and (y <= z)
-x^2 <--> -(x^2)
x^y^z <--> x^(y^z)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
a = 20
b = 10
c = 15
d = 5

e = (a + b) * c / d;-- ( 30 * 15 ) / 5
print("(a + b) * c / d 运算值为 :",e )

e = ((a + b) * c) / d; -- (30 * 15 ) / 5
print("((a + b) * c) / d 运算值为 :",e )

e = (a + b) * (c / d);-- (30) * (15/5)
print("(a + b) * (c / d) 运算值为 :",e )

e = a + (b * c) / d; -- 20 + (150/5)
print("a + (b * c) / d 运算值为 :",e )

输出:

1
2
3
4
(a + b) * c / d 运算值为  :    90.0
((a + b) * c) / d 运算值为 : 90.0
(a + b) * (c / d) 运算值为 : 90.0
a + (b * c) / d 运算值为 : 50.0

字符串

字符串或串(String)是由数字、字母、下划线组成的一串字符。

Lua 语言中字符串可以使用以下三种方式来表示:

  • 单引号间的一串字符。
  • 双引号间的一串字符。
  • [[ 与 ]] 间的一串字符。

转义字符用于表示不能直接显示的字符,比如后退键,回车键,等。如在字符串转换双引号可以使用 “"“。

微信截图_20201019165254.png

字符串操作

1
2
string.upper(argument) -- 字符串全部转为大写字母。
string.lower(argument) -- 字符串全部转为小写字母。

字符串截取

字符串截取使用 sub() 方法。

1
2
string.sub() 用于截取字符串,原型为:
string.sub(s, i [, j])

参数说明:

  • s:要截取的字符串。
  • i:截取开始位置。
  • j:截取结束位置,默认为 -1,最后一个字符。

字符串大小写转换

1
2
3
string1 = "Lua";
print(string.upper(string1))
print(string.lower(string1))

字符串查找与反转

1
2
3
4
5
string = "Lua Tutorial"
-- 查找字符串
print(string.find(string,"Tutorial"))
reversedString = string.reverse(string)
print("新字符串为",reversedString)

字符串格式化

string.format() 函数来生成具有特定格式的字符串

字符与整数相互转换

1
2
3
4
5
6
7
8
9
10
11
12
13
14
-- 字符转换
-- 转换第一个字符
print(string.byte("Lua"))
-- 转换第三个字符
print(string.byte("Lua",3))
-- 转换末尾第一个字符
print(string.byte("Lua",-1))
-- 第二个字符
print(string.byte("Lua",2))
-- 转换末尾第二个字符
print(string.byte("Lua",-2))

-- 整数 ASCII 码转换为字符
print(string.char(97))

输出:

1
2
3
4
5
6
76
97
97
117
117
a

其他常用函数

1
2
3
4
5
6
- 字符串长度
print("字符串长度 ",string.len(string2))

-- 字符串复制 2 次
repeatedString = string.rep(string2,2)
print(repeatedString)

匹配模式

Lua 中的匹配模式直接用常规的字符串来描述。 它用于模式匹配函数 string.find, string.gmatch, string.gsub, string.match。

table(表)

1
2
3
4
5
6
7
8
9
-- 初始化表
mytable = {}

-- 指定值
mytable[1]= "Lua"

-- 移除引用
mytable = nil
-- lua 垃圾回收会释放内存

参考:

https://www.runoob.com/lua/lua-tables.html

基本命令

1
vimtutor // vim 教程

移动光标

1
2
3
4
5
6
7
8
9
10
# hjkl
# 2w 向前移动两个单词
# 3e 向前移动到第 3 个单词的末尾
# 0 移动到行首
# $ 当前行的末尾
# gg 文件第一行
# G 文件最后一行
# 行号+G 指定行
# <ctrl>+o 跳转回之前的位置
# <ctrl>+i 返回跳转之前的位置

退出

1
2
3
# <esc> 进入正常模式
# :q! 不保存退出
# :wq 保存后退出

删除

1
2
3
4
5
6
# x 删除当前字符
# dw 删除至当前单词末尾
# de 删除至当前单词末尾,包括当前字符
# d$ 删除至当前行尾
# dd 删除整行
# 2dd 删除两行

修改

1
2
3
4
# i 插入文本
# A 当前行末尾添加
# r 替换当前字符
# o 打开新的一行并进入插入模式

撤销

1
2
3
4
5
6
7
8
# u 撤销
# <ctrl>+r 取消撤销
复制粘贴剪切
# v 进入可视模式
# y 复制
# p 粘贴
# yy 复制当前行
# dd 剪切当前行

状态

1
2
3
4
5
6
7
8
9
#<ctrl>+g 显示当前行以及文件信息
查找
# / 正向查找(n:继续查找,N:相反方向继续查找)
# ? 逆向查找
# % 查找配对的 {,[,(
# :set ic 忽略大小写
# :set noic 取消忽略大小写
# :set hls 匹配项高亮显示
# :set is 显示部分匹配

替换

1
2
3
# :s/old/new 替换该行第一个匹配串
# :s/old/new/g 替换全行的匹配串
# :%s/old/new/g 替换整个文件的匹配串

折叠

1
2
3
4
# zc 折叠
# zC 折叠所有嵌套
# zo 展开折叠
# zO 展开所有折叠嵌套

执行外部命令

1
# :!shell 执行外部命令

.vimrc

.vimrc 是 Vim 的配置文件,需要我们自己创建:

1
2
3
4
5
6
7
8
9
10
11
cd Home               // 进入 Home 目录
touch .vimrc // 配置文件

# Unix
# vim-plug
# Vim
curl -fLo ~/.vim/autoload/plug.vim --create-dirs \
https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim
# Neovim
curl -fLo ~/.local/share/nvim/site/autoload/plug.vim --create-dirs \
https://raw.githubusercontent.com/junegunn/vim-plug/master/plug.vim

https://github.com/junegunn/vim-plug

https://github.com/FengShangWuQi/to-vim/blob/master/.vimrc

基本配置

取消备份

1
2
set nobackup
set noswapfile

文件编码

1
setencoding=utf-8

显示行号

setnumber

取消换行

setnowrap

显示光标当前位置

setruler

设置缩进

1
2
3
set cindent
set tabstop=2
set shiftwidth=2

突出显示当前行

setcursorline

查找

1
2
3
set ic
set hls
set is

左下角显示当前vim模式

setshowmode

代码折叠

1
2
#启动 vim 时关闭折叠代码
set nofoldenable

主题

1
2
3
syntax enable
set background=dark
colorscheme solarized

https://github.com/altercation/vim-colors-solarized
https://github.com/Anthony25/gnome-terminal-colors-solarized

插件配置

树形目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
Plug 'scrooloose/nerdtree'
Plug 'jistr/vim-nerdtree-tabs'
Plug 'Xuyuanp/nerdtree-git-plugin'

autocmd vimenter * NERDTree
map <C-n> :NERDTreeToggle<CR>
let NERDTreeShowHidden=1
let g:NERDTreeShowIgnoredStatus = 1
let g:nerdtree_tabs_open_on_console_startup=1
let g:NERDTreeIndicatorMapCustom = {
\ "Modified" : "✹",
\ "Staged" : "✚",
\ "Untracked" : "✭",
\ "Renamed" : "➜",
\ "Unmerged" : "═",
\ "Deleted" : "✖",
\ "Dirty" : "✗",
\ "Clean" : "✔︎",
\ 'Ignored' : '☒',
\ "Unknown" : "?"
\ }

# o 打开关闭文件或目录
# e 以文件管理的方式打开选中的目录
# t 在标签页中打开
# T 在标签页中打开,但光标仍然留在 NERDTree
# r 刷新光标所在的目录
# R 刷新当前根路径
# X 收起所有目录
# p 小写,跳转到光标所在的上一级路径
# P 大写,跳转到当前根路径
# J 到第一个节点
# K 到最后一个节点
# I 显示隐藏文件
# m 显示文件操作菜单
# C 将根路径设置为光标所在的目录
# u 设置上级目录为根路径
# ctrl + w + w 光标自动在左右侧窗口切换
# ctrl + w + r 移动当前窗口的布局位置
# :tabc 关闭当前的 tab
# :tabo 关闭所有其他的 tab
# :tabp 前一个 tab
# :tabn 后一个 tab
# gT 前一个 tab
# gt 后一个 tab

https://github.com/scrooloose/nerdtree
https://github.com/jistr/vim-nerdtree-tabs
https://github.com/Xuyuanp/nerdtree-git-plugin

代码,引号,路径补全

1
2
3
Plug 'Valloric/YouCompleteMe'
Plug 'Raimondi/delimitMate'
Plug 'Shougo/deoplete.nvim', { 'do': ':UpdateRemotePlugins' }

https://github.com/Valloric/YouCompleteMe
https://github.com/Raimondi/delimitMate
https://github.com/Shougo/deoplete.nvim

语法高亮,检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Plug 'sheerun/vim-polyglot'
Plug 'w0rp/ale'

let g:ale_linters = {
\ 'javascript': ['eslint'],
\ 'css': ['stylelint'],
\}
let g:ale_fixers = {
\ 'javascript': ['eslint'],
\ 'css': ['stylelint'],
\}
let g:ale_fix_on_save = 1

let g:ale_sign_column_always = 1
let g:ale_sign_error = '●'
let g:ale_sign_warning = '▶'

nmap <silent> <C-k> <Plug>(ale_previous_wrap)
nmap <silent> <C-j> <Plug>(ale_next_wrap)

https://github.com/w0rp/ale
https://github.com/sheerun/vim-polyglot

文件,代码搜索

1
2
Plug 'rking/ag.vim'
Plug 'kien/ctrlp.vim'

https://github.com/kien/ctrlp.vim
https://github.com/ggreer/the_silver_searcher
https://github.com/rking/ag.vim

加强版状态栏

1
2
3
4
Plug 'vim-airline/vim-airline'
Plug 'vim-airline/vim-airline-themes'

let g:airline_theme='papercolor'

https://github.com/vim-airline/vim-airline
https://github.com/vim-airline/vim-airline-themes

代码注释

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Plug 'scrooloose/nerdcommenter'

# <leader>cc // 注释
# <leader>cm 只用一组符号注释
# <leader>cA 在行尾添加注释
# <leader>c$ /* 注释 */
# <leader>cs /* 块注释 */
# <leader>cy 注释并复制
# <leader>c<space> 注释/取消注释
# <leader>ca 切换 // 和 /* */
# <leader>cu 取消注释

let g:NERDSpaceDelims = 1
let g:NERDDefaultAlign = 'left'
let g:NERDCustomDelimiters = {
\ 'javascript': { 'left': '//', 'leftAlt': '/**', 'rightAlt': '*/' },
\ 'less': { 'left': '/**', 'right': '*/' }
\ }

https://github.com/scrooloose/nerdcommenter

git

1
2
Plug 'airblade/vim-gitgutter'
Plug 'tpope/vim-fugitive'

https://github.com/airblade/vim-gitgutter
https://github.com/tpope/vim-fugitive

Markdown

1
2
3
4
5
Plug 'suan/vim-instant-markdown'

let g:instant_markdown_slow = 1
let g:instant_markdown_autostart = 0
# :InstantMarkdownPreview

https://github.com/suan/vim-instant-markdown

Emmet

1
2
3
4
5
6
7
8
Plug 'mattn/emmet-vim'

let g:user_emmet_leader_key='<Tab>'
let g:user_emmet_settings = {
\ 'javascript.jsx' : {
\ 'extends' : 'jsx',
\ },
\ }

https://github.com/mattn/emmet-vim

html 5

1
Plug'othree/html5.vim'

https://github.com/othree/html5.vim

css 3

1
2
3
4
5
6
7
8
Plug 'hail2u/vim-css3-syntax'
Plug 'ap/vim-css-color'

augroup VimCSS3Syntax
autocmd!

autocmd FileType css setlocal iskeyword+=-
augroup END

https://github.com/hail2u/vim-css3-syntax
https://github.com/ap/vim-css-color

JavaScipt

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Plug 'pangloss/vim-javascript'
let g:javascript_plugin_jsdoc = 1
let g:javascript_plugin_ngdoc = 1
let g:javascript_plugin_flow = 1
set foldmethod=syntax
let g:javascript_conceal_function = "ƒ"
let g:javascript_conceal_null = "ø"
let g:javascript_conceal_this = "@"
let g:javascript_conceal_return = "⇚"
let g:javascript_conceal_undefined = "¿"
let g:javascript_conceal_NaN = "ℕ"
let g:javascript_conceal_prototype = "¶"
let g:javascript_conceal_static = "•"
let g:javascript_conceal_super = "Ω"
let g:javascript_conceal_arrow_function = "⇒"
let g:javascript_conceal_noarg_arrow_function = " "
let g:javascript_conceal_underscore_arrow_function = " "
set conceallevel=1

https://github.com/pangloss/vim-javascript
(注:上述脚本中存在特殊字符,有的情况下显示不正确,请直接用上述链接的内容。)

React

1
2
Plug 'mxw/vim-jsx'
let g:jsx_ext_required = 0

https://github.com/mxw/vim-jsx

Prettier

1
2
3
4
5
6
7
8
Plug 'prettier/vim-prettier', {
\ 'do': 'yarn install',
\ 'for': ['javascript', 'typescript', 'css', 'less', 'scss', 'json', 'graphql'] }
let g:prettier#config#bracket_spacing = 'true'
let g:prettier#config#jsx_bracket_same_line = 'false'
let g:prettier#autoformat = 0
autocmd BufWritePre *.js,*.jsx,*.mjs,*.ts,*.tsx,*.css,*.less,*.scss,*.json,*.graphql PrettierAsync
# :Prettier

https://github.com/prettier/vim-prettier

转载:

如何让 vim 成为我们的神器

托管类型

托管类型包括 引用类型 以及 包含有引用类型或托管类型成员的结构。

  • 引用类型
  • 含引用类型或托管类型成员(字段、自动实现 get 访问器的属性)的结构(managed structure)

非托管类型

如果某个类型是以下类型之一,则它是非托管类型 :

  • sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double、decimal 或 bool
  • 任何枚举类型
  • 任何指针类型
  • 任何用户定义的 struct 类型,只包含非托管类型的字段,并且在 C# 7.3 及更早版本中,不是构造类型(包含至少一个类型参数的类型)

参考:

非托管类型(C# 参考)

“.NET Core 3.x”和“.NET Standard 2.1”支持 C# 8.0 。有关详细信息,请参阅C# 语言版本控制

Readonly 成员

可将 readonly 修饰符应用于结构的成员。 它指示该成员不会修改状态。 这比将 readonly 修饰符应用于 struct 声明更精细。

1
2
3
4
5
6
7
8
9
public struct Point
{
public double X { get; set; }
public double Y { get; set; }
public readonly double Distance => Math.Sqrt(X * X + Y * Y);

public readonly override string ToString() =>
$"({X}, {Y}) is {Distance} from the origin";
}

readonly 修饰符对于只读属性是必需的。 编译器会假设 get 访问器可以修改状态;必须显式声明 readonly。 自动实现的属性是一个例外;编译器会将所有自动实现的 Getter 视为 readonly,因此,此处无需向 X 和 Y 属性添加 readonly 修饰符。

默认接口方法

现在可以将成员添加到接口,并为这些成员提供实现。 借助此语言功能,API 作者可以将方法添加到以后版本的接口中,而不会破坏与该接口当前实现的源或二进制文件兼容性。 现有的实现继承默认实现。

参考:

https://github.com/dotnet/samples/tree/master/csharp/tutorials/default-interface-members-versions/starter/customer-relationship

[教程:在 C# 8.0 中使用默认接口方法更新接口](https://docs.microsoft.com/zh-cn/dotnet/csharp/tutorials/default-interface-methods-versions

switch 表达式

1
2
3
4
5
6
7
8
9
10
11
12
public static RGBColor FromRainbow(Rainbow colorBand) =>
colorBand switch
{
Rainbow.Red => new RGBColor(0xFF, 0x00, 0x00),
Rainbow.Orange => new RGBColor(0xFF, 0x7F, 0x00),
Rainbow.Yellow => new RGBColor(0xFF, 0xFF, 0x00),
Rainbow.Green => new RGBColor(0x00, 0xFF, 0x00),
Rainbow.Blue => new RGBColor(0x00, 0x00, 0xFF),
Rainbow.Indigo => new RGBColor(0x4B, 0x00, 0x82),
Rainbow.Violet => new RGBColor(0x94, 0x00, 0xD3),
_ => throw new ArgumentException(message: "invalid enum value", paramName: nameof(colorBand)),
};
  • 变量位于 switch 关键字之前。 不同的顺序使得在视觉上可以很轻松地区分 switch 表达式和 switch 语句。
  • 将 case 和 : 元素替换为 =>。 它更简洁,更直观。
  • 将 default 事例替换为 _ 弃元。
  • 正文是表达式,不是语句。

属性模式

1
2
3
4
5
6
7
8
9
public static decimal ComputeSalesTax(Address location, decimal salePrice) =>
location switch
{
{ State: "WA" } => salePrice * 0.06M,
{ State: "MN" } => salePrice * 0.075M,
{ State: "MI" } => salePrice * 0.05M,
// other cases removed for brevity...
_ => 0M
};

元组模式

1
2
3
4
5
6
7
8
9
10
11
public static string RockPaperScissors(string first, string second)
=> (first, second) switch
{
("rock", "paper") => "rock is covered by paper. Paper wins.",
("rock", "scissors") => "rock breaks scissors. Rock wins.",
("paper", "rock") => "paper covers rock. Paper wins.",
("paper", "scissors") => "paper is cut by scissors. Scissors wins.",
("scissors", "rock") => "scissors is broken by rock. Rock wins.",
("scissors", "paper") => "scissors cuts paper. Scissors wins.",
(_, _) => "tie"
};

位置模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class Point
{
public int X { get; }
public int Y { get; }

public Point(int x, int y) => (X, Y) = (x, y);

public void Deconstruct(out int x, out int y) =>
(x, y) = (X, Y);
}

public enum Quadrant
{
Unknown,
Origin,
One,
Two,
Three,
Four,
OnBorder
}

static Quadrant GetQuadrant(Point point) => point switch
{
(0, 0) => Quadrant.Origin,
var (x, y) when x > 0 && y > 0 => Quadrant.One,
var (x, y) when x < 0 && y > 0 => Quadrant.Two,
var (x, y) when x < 0 && y < 0 => Quadrant.Three,
var (x, y) when x > 0 && y < 0 => Quadrant.Four,
var (_, _) => Quadrant.OnBorder,
_ => Quadrant.Unknown
};

using 声明

using 声明是前面带 using 关键字的变量声明。它指示编译器声明的变量应在封闭范围的末尾进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
static int WriteLinesToFile(IEnumerable<string> lines)
{
// 当到达方法的右括号时,将对该文件进行处理。
using var file = new System.IO.StreamWriter("WriteLines2.txt");
// Notice how we declare skippedLines after the using statement.
int skippedLines = 0;
foreach (string line in lines)
{
if (!line.Contains("Second"))
{
file.WriteLine(line);
}
else
{
skippedLines++;
}
}
// Notice how skippedLines is in scope here.
return skippedLines;
// file is disposed here
}

静态本地函数

1
2
3
4
5
6
7
8
int M()
{
int y = 5;
int x = 7;
return Add(x, y);

static int Add(int left, int right) => left + right;
}

可处置的 ref 结构

用 ref 修饰符声明的 struct 可能无法实现任何接口,因此无法实现 IDisposable。 因此,要能够处理 ref struct,它必须有一个可访问的 void Dispose() 方法。 此功能同样适用于 readonly ref struct 声明。

可为空引用类型

异步流

从 C# 8.0 开始,可以创建并以异步方式使用流。 返回异步流的方法有三个属性:

  • 它是用 async 修饰符声明的。
  • 它将返回 IAsyncEnumerable
  • 该方法包含用于在异步流中返回连续元素的 yield return 语句。

使用异步流需要在枚举流元素时在 foreach 关键字前面添加 await 关键字。 添加 await 关键字需要枚举异步流的方法,以使用 async 修饰符进行声明并返回 async 方法允许的类型。 通常这意味着返回 Task 或 Task。 也可以为 ValueTask 或 ValueTask。 方法既可以使用异步流,也可以生成异步流,这意味着它将返回 IAsyncEnumerable

1
2
3
4
await foreach (var number in GenerateSequence())
{
Console.WriteLine(number);
}

异步可释放

从 C# 8.0 开始,语言支持实现 System.IAsyncDisposable 接口的异步可释放类型。 可使用 await using 语句来处理异步可释放对象。

索引和范围

索引和范围为访问序列中的单个元素或范围提供了简洁的语法。

此语言支持依赖于两个新类型和两个新运算符:

  • System.Index 表示一个序列索引。
  • 来自末尾运算符 ^ 的索引,指定一个索引与序列末尾相关。
  • System.Range 表示序列的子范围。
  • 范围运算符 ..,用于指定范围的开始和末尾,就像操作数一样。

0 索引与 sequence[0] 相同。 ^0 索引与 sequence[sequence.Length] 相同。 请注意,sequence[^0] 不会引发异常,就像 sequence[sequence.Length] 一样。 对于任何数字 n,索引 ^n 与 sequence.Length - n 相同。

范围指定范围的开始和末尾 。 包括此范围的开始,但不包括此范围的末尾,这表示此范围包含开始但不包含末尾 。 范围 [0..^0] 表示整个范围,就像 [0..sequence.Length] 表示整个范围。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var words = new string[]
{
// index from start index from end
"The", // 0 ^9
"quick", // 1 ^8
"brown", // 2 ^7
"fox", // 3 ^6
"jumped", // 4 ^5
"over", // 5 ^4
"the", // 6 ^3
"lazy", // 7 ^2
"dog" // 8 ^1
}; // 9 (or words.Length) ^0

// 使用 ^1 索引检索最后一个词
Console.WriteLine($"The last word is {words[^1]}");
// writes "dog"

// 包括 words[1] 到 words[3]
var quickBrownFox = words[1..4];

// 包括 words[^2] 和 words[^1]
var lazyDog = words[^2..^0];

var allWords = words[..]; // contains "The" through "dog".
var firstPhrase = words[..4]; // contains "The" through "fox"
var lastPhrase = words[6..]; // contains "the", "lazy" and "dog"

Range phrase = 1..4;
var text = words[phrase];

Null 合并赋值

C# 8.0 引入了 null 合并赋值运算符 ??=。 仅当左操作数计算为 null 时,才能使用运算符 ??= 将其右操作数的值分配给左操作数。

1
2
3
4
5
6
7
8
9
List<int> numbers = null;
int? i = null;

numbers ??= new List<int>();
numbers.Add(i ??= 17);
numbers.Add(i ??= 20);

Console.WriteLine(string.Join(" ", numbers)); // output: 17 17
Console.WriteLine(i); // output: 17

非托管构造类型

在 C# 7.3 及更低版本中,构造类型(包含至少一个类型参数的类型)不能为非托管类型。 从 C# 8.0 开始,如果构造的值类型仅包含非托管类型的字段,则该类型不受管理。

1
2
3
4
5
public struct Coords<T>
{
public T X;
public T Y;
}

Coords 类型为 C# 8.0 及更高版本中的非托管类型。 与任何非托管类型一样,可以创建指向此类型的变量的指针,或针对此类型的实例在堆栈上分配内存块:

1
2
3
4
5
6
Span<Coords<int>> coordinates = stackalloc[]
{
new Coords<int> { X = 0, Y = 0 },
new Coords<int> { X = 0, Y = 3 },
new Coords<int> { X = 4, Y = 0 }
};

嵌套表达式中的 stackalloc

stackalloc 表达式在堆栈上分配内存块。该方法返回时,将自动丢弃在方法执行期间创建的堆栈中分配的内存块。 不能显式释放使用 stackalloc 分配的内存。 堆栈中分配的内存块不受垃圾回收的影响,也不必通过 fixed 语句固定。

1
2
3
Span<int> numbers = stackalloc[] { 1, 2, 3, 4, 5, 6 };
var ind = numbers.IndexOfAny(stackalloc[] { 2, 4, 6, 8 });
Console.WriteLine(ind); // output: 1

stackalloc 表达式(C# 参考)

内插逐字字符串的增强功能

内插逐字字符串中 $ 和 @ 标记的顺序可以任意安排:$@”…” 和 @$”…” 均为有效的内插逐字字符串。 在早期 C# 版本中,$ 标记必须出现在 @ 标记之前。

参考:

C# 8.0 中的新增功能

out 变量

可以将 out 值内联作为参数声明到使用这些参数的方法中。

无需分配初始值

1
2
3
4
if (int.TryParse(input, out var answer))
Console.WriteLine(answer);
else
Console.WriteLine("Could not parse input");

元组

低于 C# 7.0 的版本中也提供元组,但它们效率低下且不具有语言支持。 这意味着元组元素只能作为 Item1 和 Item2 等引用。 C# 7.0 引入了对元组的语言支持,可利用更有效的新元组类型向元组字段赋予语义名称。这些名称仅存在于编译时且不保留,例如在运行时使用反射来检查元组时。

1
2
3
4
5
6
7
8
9
10
11
// 第一种写法
(string Alpha, string Beta) namedLetters = ("a", "b");

// 第二种写法
var alphabetStart = (Alpha: "a", Beta: "b");
Console.WriteLine($"{namedLetters.Alpha}, {namedLetters.Beta}");

// 第三种写法
(int max, int min) = Range(numbers);
Console.WriteLine(max);
Console.WriteLine(min);

从 C# 7.3 开始,元组类型支持 == 和 != 运算符。这些运算符按照元组元素的顺序将左侧操作数的成员与相应的右侧操作数的成员进行比较。

参考:

元组类型(C# 参考)

弃元

弃元是一个名为 _(下划线字符)的只写变量,可向单个变量赋予要放弃的所有值。 弃元类似于未赋值的变量;不可在代码中使用弃元(赋值语句除外)。

在以下方案中支持弃元:

  • 在对元组或用户定义的类型进行解构时。
  • 在使用 out 参数调用方法时。
  • 在使用 is 和 switch 语句匹配操作的模式中。
  • 在要将某赋值的值显式标识为弃元时用作独立标识符。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
using System;
using System.Collections.Generic;

public class Example
{
public static void Main()
{
var (_, _, _, pop1, _, pop2) = QueryCityDataForYears("New York City", 1960, 2010);

Console.WriteLine($"Population change, 1960 to 2010: {pop2 - pop1:N0}");
}

private static (string, double, int, int, int, int) QueryCityDataForYears(string name, int year1, int year2)
{
int population1 = 0, population2 = 0;
double area = 0;

if (name == "New York City")
{
area = 468.48;
if (year1 == 1960)
{
population1 = 7781984;
}
if (year2 == 2010)
{
population2 = 8175133;
}
return (name, area, year1, population1, year2, population2);
}

return ("", 0, 0, 0, 0, 0);
}
}
// The example displays the following output:
// Population change, 1960 to 2010: 393,149

模式匹配

模式匹配支持 is 表达式和 switch 表达式。 每个表达式都允许检查对象及其属性以确定该对象是否满足所寻求的模式。 使用 when 关键字来指定模式的其他规则。

1
2
if (input is int count)
sum += count;

更新后的 switch 语句有几个新构造:

  • switch 表达式的控制类型不再局限于整数类型、Enum 类型、string 或与这些类型之一对应的可为 null 的类型。 可能会使用任何类型。
  • 可以在每个 case 标签中测试 switch 表达式的类型。 与 is 表达式一样,可以为该类型指定一个新变量。
  • 可以添加 when 子句以进一步测试该变量的条件。
  • case 标签的顺序现在很重要。 执行匹配的第一个分支;其他将跳过。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static int SumPositiveNumbers(IEnumerable<object> sequence)
{
int sum = 0;
foreach (var i in sequence)
{
switch (i)
{
case 0:
break;
case IEnumerable<int> childSequence:
{
foreach(var item in childSequence)
sum += (item > 0) ? item : 0;
break;
}
case int n when n > 0:
sum += n;
break;
case null:
throw new NullReferenceException("Null found in sequence");
default:
throw new InvalidOperationException("Unrecognized type");
}
}
return sum;
}
  • case 0: 是常见的常量模式。
  • case IEnumerable childSequence: 是一种类型模式。
  • case int n when n > 0: 是具有附加 when 条件的类型模式。
  • case null: 是 null 模式。
  • default: 是常见的默认事例。

Ref 局部变量和返回结果

此功能允许使用并返回对变量的引用的算法,这些变量在其他位置定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public static ref int Find(int[,] matrix, Func<int, bool> predicate)
{
for (int i = 0; i < matrix.GetLength(0); i++)
for (int j = 0; j < matrix.GetLength(1); j++)
if (predicate(matrix[i, j]))
return ref matrix[i, j];
throw new InvalidOperationException("Not found");
}

// 修改该值
ref var item = ref MatrixSearch.Find(matrix, (val) => val == 42);
Console.WriteLine(item);
item = 24;
Console.WriteLine(matrix[4, 2]);

C# 语言还有多个规则,可保护你免于误用 ref 局部变量和返回结果:

  • 必须将 ref 关键字添加到方法签名和方法中的所有 return 语句中。表明,该方法在整个方法中通过引用返回。
  • 可以将 ref return 分配给值变量或 ref 变量。调用方控制是否复制返回值。 在分配返回值时省略 ref 修饰符表示调用方需要该值的副本,而不是对存储的引用。
  • 不可向 ref 本地变量赋予标准方法返回值。因为那将禁止类似 ref int i = sequence.Count(); 这样的语句
  • 不能将 ref 返回给其生存期不超出方法执行的变量。这意味着不可返回对本地变量或对类似作用域变量的引用。
  • ref 局部变量和返回结果不可用于异步方法。编译器无法知道异步方法返回时,引用的变量是否已设置为其最终值。

添加 ref 局部变量和 ref 返回结果可通过避免复制值或多次执行取消引用的操作,允许更为高效的算法。

本地函数

方法套方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public Task<string> PerformLongRunningWork(string address, int index, string name)
{
if (string.IsNullOrWhiteSpace(address))
throw new ArgumentException(message: "An address is required", paramName: nameof(address));
if (index < 0)
throw new ArgumentOutOfRangeException(paramName: nameof(index), message: "The index must be non-negative");
if (string.IsNullOrWhiteSpace(name))
throw new ArgumentException(message: "You must supply a name", paramName: nameof(name));

return longRunningWorkImplementation();

async Task<string> longRunningWorkImplementation()
{
var interimResult = await FirstWork(address);
var secondResult = await SecondStep(index, name);
return $"The results are {interimResult} and {secondResult}. Enjoy.";
}
}

更多的 expression-bodied 成员

C# 7.0 扩展了可作为表达式实现的允许的成员。 在 C# 7.0 中,你可以在属性和索引器上实现构造函数、终结器以及 get 和 set 访问器。 以下代码演示了每种情况的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Expression-bodied constructor
public ExpressionMembersExample(string label) => this.Label = label;

// Expression-bodied finalizer
~ExpressionMembersExample() => Console.Error.WriteLine("Finalized!");

private string label;

// Expression-bodied get / set accessors.
public string Label
{
get => label;
set => this.label = value ?? "Default label";
}

本示例不需要终结器,但显示它是为了演示语法。 不应在类中实现终结器,除非有必要发布非托管资源。 还应考虑使用 SafeHandle 类,而不是直接管理非托管资源。

throw 表达式

  • 条件运算符。 下例使用 throw 表达式在向方法传递空字符串数组时引发 ArgumentException。 在 C# 7.0 之前,此逻辑将需要显示在 if/else 语句中。
1
2
3
4
5
6
7
8
9
private static void DisplayFirstNumber(string[] args)
{
string arg = args.Length >= 1 ? args[0] :
throw new ArgumentException("You must supply an argument");
if (Int64.TryParse(arg, out var number))
Console.WriteLine($"You entered {number:F0}");
else
Console.WriteLine($"{arg} is not a number.");
}
  • null 合并运算符。 在以下示例中,如果分配给 Name 属性的字符串为 null,则将 throw 表达式与 null 合并运算符结合使用以引发异常。
1
2
3
4
5
6
public string Name
{
get => name;
set => name = value ??
throw new ArgumentNullException(paramName: nameof(value), message: "Name cannot be null");
}
  • expression-bodied lambda 或方法。 下例说明了 expression-bodied 方法,由于不支持对 DateTime 值的转换,该方法引发 InvalidCastException。
1
2
DateTime ToDateTime(IFormatProvider provider) =>
throw new InvalidCastException("Conversion to a DateTime is not supported.");

通用的异步返回类型

添加 NuGet 包 System.Threading.Tasks.Extensions 才能使用 ValueTask 类型。

新语言功能意味着异步方法返回类型不限于 Task、Task 和 void。

1
2
3
4
5
public async ValueTask<int> Func()
{
await Task.Delay(100);
return 5;
}

数字文本语法改进

C# 7.0 包括两项新功能,可用于以最可读的方式写入数字来用于预期用途:二进制文本和数字分隔符 。
在创建位掩码时,或每当数字的二进制表示形式使代码最具可读性时,以二进制形式写入该数字:

1
2
3
4
public const int Sixteen =   0b0001_0000;
public const int ThirtyTwo = 0b0010_0000;
public const int SixtyFour = 0b0100_0000;
public const int OneHundredTwentyEight = 0b1000_0000;

常量开头的 0b 表示该数字以二进制数形式写入。 二进制数可能会很长,因此通过引入 _ 作为数字分隔符通常更易于查看位模式,如上面二进制常量所示。

数字分隔符可以出现在常量的任何位置。 对于十进制数字,通常将其用作千位分隔符:

1
2
3
4
5
public const long BillionsAndBillions = 100_000_000_000;

// 数字分隔符也可以与 decimal、float 和 double 类型一起使用:
public const double AvogadroConstant = 6.022_140_857_747_474e23;
public const decimal GoldenRatio = 1.618_033_988_749_894_848_204_586_834_365_638_117_720_309_179M;

综观来说,你可以声明可读性更强的数值常量。

参考:

C# 7.0 中的新增功能

只读自动属性

只读自动属性 提供了更简洁的语法来创建不可变类型。

1
2
public string FirstName { get; }
public string LastName { get; }

FirstName 和 LastName 属性只能在同一个类的构造函数的主体中设置:

1
2
3
4
5
6
7
public Student(string firstName, string lastName)
{
if (IsNullOrWhiteSpace(lastName))
throw new ArgumentException(message: "Cannot be blank", paramName: nameof(lastName));
FirstName = firstName;
LastName = lastName;
}

尝试在另一种方法中设置 LastName 会生成 CS0200 编译错误:

1
2
3
4
5
6
7
8
9
10
public class Student
{
public string LastName { get; }

public void ChangeName(string newLastName)
{
// Generates CS0200: Property or indexer cannot be assigned to -- it is read only
LastName = newLastName;
}
}

自动属性初始化

自动属性初始值设定项 可让你在属性声明中声明自动属性的初始值。

1
public ICollection<double> Grades { get; } = new List<double>();

Grades 成员在声明它的位置处被初始化。 这样,就能更容易地仅执行一次初始化。 初始化是属性声明的一部分,可更轻松地将存储分配等同于 Student 对象的公用接口。

Expression-bodied 函数成员

编写的许多成员是可以作为单个表达式的单个语句。 改为编写 expression-bodied 成员。 这适用于方法和只读属性。

1
2
3
4
5
// 方法
public override string ToString() => $"{LastName}, {FirstName}";

// 也可以将此语法用于只读属性:
public string FullName => $"{FirstName} {LastName}";

using static

using static 增强功能可用于导入单个类的静态方法。 指定要使用的类:

1
2
3
using static System.Math;
using static System.Linq.Enumerable;
using static System.String;

备注:在 static using 语句中必须使用完全限定的类名 System.String。 而不能使用 string 关键字。

在 LINQ 查询中会经常看到这种情况。 可以通过导入 Enumerable 或 Queryable 来导入 LINQ 模式。

using static 语法导入一个类型,然后就可以在其全局作用域范围内(当前文件内)使用它可以访问(遵循访问修饰符的限定)类型的静态成员了,需要注意的几点是:

  • 导入的成员签名和现有的成员签名相同时,使用现有的成员。
  • 导入的成员之间出现成员签名相同的情况,使用的时候会编译不通过,需要一处一个 using static 才可,或者改为正常的调用方式。
  • class,struct,emun 类型可以使用 using static 导入。
  • 静态属性,字段,事件等等,,,静态成员均可依靠 using static 省略类型前缀。
  • 扩展方法也可以使用 using static,但是需要按照实例方法的调用方式来使用。

参考:[C#6] 1-using static

Null 条件运算符

1
2
3
4
5
6
7
8
9
// 如果 Person 对象是 null,则将变量 first 赋值为 null。 否则,将 FirstName 属性的值分配给该变量。
var first = person?.FirstName;

// 无论 person 的值是什么,以下表达式均返回 string。
// 通常,将此构造与 null coalescing (null 合并) 运算符一起使用
first = person?.FirstName ?? "Unspecified";

// 调用该委托
this.SomethingHappened?.Invoke(this, eventArgs);

字符串内插

关键符号:${}

1
public string FullName => $"{FirstName} {LastName}";

异常筛选器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public static async Task<string> MakeRequest()
{
WebRequestHandler webRequestHandler = new WebRequestHandler();
webRequestHandler.AllowAutoRedirect = false;
using (HttpClient client = new HttpClient(webRequestHandler))
{
var stringTask = client.GetStringAsync("https://docs.microsoft.com/en-us/dotnet/about/");
try
{
var responseText = await stringTask;
return responseText;
}
catch (System.Net.Http.HttpRequestException e) when (e.Message.Contains("301"))
{
return "Site Moved";
}
}
}

nameof 表达式

nameof 表达式的计算结果为符号的名称。

1
2
if (IsNullOrWhiteSpace(lastName))
throw new ArgumentException(message: "Cannot be blank", paramName: nameof(lastName));

Catch 和 Finally 块中的 Await

C# 5 对于可放置 await 表达式的位置有若干限制。 使用 C# 6,现在可以在 catch 或 finally 表达式中使用 await。
鉴于此行为,建议仔细编写 catch 和 finally 子句,避免引入新的异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static async Task<string> MakeRequestAndLogFailures()
{
await logMethodEntrance();
var client = new System.Net.Http.HttpClient();
var streamTask = client.GetStringAsync("https://localHost:10000");
try {
var responseText = await streamTask;
return responseText;
} catch (System.Net.Http.HttpRequestException e) when (e.Message.Contains("301"))
{
await logError("Recovered from redirect", e);
return "Site Moved";
}
finally
{
await logMethodExit();
client.Dispose();
}
}

使用索引器初始化关联集合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 原来语法
private Dictionary<int, string> messages = new Dictionary<int, string>
{
{ 404, "Page not Found"},
{ 302, "Page moved, but left a forwarding address."},
{ 500, "The web server can't come out to play today."}
};

// 新语法支持使用索引分配到集合中:
private Dictionary<int, string> webErrors = new Dictionary<int, string>
{
[404] = "Page not Found",
[302] = "Page moved, but left a forwarding address.",
[500] = "The web server can't come out to play today."
};

此功能意味着,可以使用与多个版本中已有的序列容器语法类似的语法初始化关联容器。

参考:

C# 6 中的新增功能

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
using MongoDB.Bson;
using MongoDB.Driver;
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Threading.Tasks;

namespace MongoDBTest
{
internal class Program
{
private const string MongoDBConnection = "mongodb://localhost:27017/admin";

private static IMongoClient _client = new MongoClient(MongoDBConnection);
private static IMongoDatabase _database = _client.GetDatabase("Test");
private static IMongoCollection<CollectionModel> _collection = _database.GetCollection<CollectionModel>("TestCollection");

private static async Task Main(string[] args)
{
// 添加
await InsertOneAsyncTest();

Console.WriteLine("-----------FindAsyncTest输出---------------");
await FindAsyncTest();

Console.WriteLine("-----------Query输出---------------");
await Query("a");

Console.WriteLine("-----------SkipAndSortAndLimitAndProjections输出---------------");
var request = new PageRequest {PageIndex = 1, PageSize = 2};
await SkipAndSortAndLimitAndProjections(request);

Console.ReadKey();
}

/// <summary>
/// 查询,支持模糊查询
/// </summary>
/// <param name="keyword"></param>
/// <returns></returns>
private static async Task Query(string keyword)
{
FilterDefinitionBuilder<CollectionModel> builder = Builders<CollectionModel>.Filter;
string p = keyword == null ? $".*{Regex.Escape("")}.*" : $".*{Regex.Escape(keyword)}.*";

//1,条件为空
//FilterDefinition<CollectionModel> filter2 = FilterDefinition<CollectionModel>.Empty;

//2,不区分大小写
FilterDefinition<CollectionModel> filter2 = builder.Regex("title", new BsonRegularExpression(new Regex(p, RegexOptions.IgnoreCase)));

// 以下两种写法区分大小写
//FilterDefinition<CollectionModel> filter2 = builder.Regex("title", new BsonRegularExpression(p));
//BsonDocument filter2 = new BsonDocument { { "title", $"/{keyword}/" } };

FindOptions<CollectionModel> options = new FindOptions<CollectionModel>
{
BatchSize = 2,
NoCursorTimeout = false
};
var cursor = await _collection.FindAsync(filter2, options);
var batch = 0;
while (await cursor.MoveNextAsync())
{
batch++;
Console.WriteLine($"Batch: {batch}");
IEnumerable<CollectionModel> documents = cursor.Current;
foreach (CollectionModel document in documents)
{
Console.WriteLine(document.ToJson());
Console.WriteLine();
}
}
}

/// <summary>
/// Skip, Sort, Limit, Projections
/// </summary>
/// <param name="request"></param>
/// <returns></returns>
private static async Task SkipAndSortAndLimitAndProjections(PageRequest request)
{
var count = 0;
await _collection.Find(FilterDefinition<CollectionModel>.Empty)
.Skip((request.PageIndex - 1) * request.PageSize)
.Limit(request.PageSize)
//.Sort(Builders<CollectionModel>.Sort.Descending("title"))
//.Sort(Builders<CollectionModel>.Sort.Descending(x => x.title).Ascending(x => x.content))
.Sort("{title: 1}")
.Project(x => new { x.m_id, x.title, x.content, x.userinfo })
.ForEachAsync(
obj =>
{
Console.WriteLine($"S/N: {count}, \t Id: {obj.m_id}, title: {obj.title}, content: {obj.content}, serinfo.name: {obj?.userinfo?.name}");
count++;
});
}

private static async Task InsertOneAsyncTest()
{
CollectionModel model = new CollectionModel();
model.title = "A1";
model.content = "可以测试";
model.userinfo = new UserModel
{
name = "测试",
addressList = new List<AddressModel>
{
new AddressModel { address = "北京" },
new AddressModel { address = "上海" }
}
};
await _collection.InsertOneAsync(model);
}

private static async Task FindAsyncTest()
{
using IAsyncCursor<CollectionModel> cursor = await _collection.FindAsync(new BsonDocument());
while (await cursor.MoveNextAsync())
{
IEnumerable<CollectionModel> batch = cursor.Current;
foreach (CollectionModel document in batch)
{
Console.WriteLine(document.ToJson());
Console.WriteLine();
}
}
}
}
}

公共类

做通用查询的时候,可以使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
/// <summary>
/// 公共页面请求参数
/// </summary>
public class PageRequest
{
public int PageIndex { get; set; }

public int PageSize { get; set; }

public string Order { get; set; }
}

/// <summary>
/// 公用页面参数
/// </summary>
public class ParameterBase
{
public string CollectName { get; set; }

public int PageIndex { get; set; }

public int PageSize { get; set; }
}

/// <summary>
/// 公用页面参数-MongoDB
/// </summary>
public class PageMongoParameter : ParameterBase
{
public BsonDocument Sort { get; }
public BsonDocument Skip { get; }
public BsonDocument Limit { get; }
public BsonDocument Filter2 { get; }

public BsonDocument[] Pipeline { get; set; }

public PageMongoParameter(PageRequest req, string collectName, BsonDocument filter = null,
BsonDocument sort = null, BsonDocument filter2 = null)
{
CollectName = collectName;
PageIndex = req.PageIndex;
PageSize = req.PageSize;
Limit = new BsonDocument { { "$limit", PageSize } };
Skip = new BsonDocument { { "$skip", (PageIndex - 1) * PageSize } };

if (sort == null)
{
if (!string.IsNullOrEmpty(req.Order)) Sort = new BsonDocument { { "$sort", new BsonDocument { { req.Order, 1 } } } };
}
else
{
Sort = sort;
}

if (filter != null && filter2 == null)
{
var dom = filter.GetElement(0);
Filter2 = BsonDocument.Parse(dom.Value.ToJson());
}
Filter2 ??= new BsonDocument();

if (filter == null && Sort == null)
Pipeline = new[] { Skip, Limit };
else
{
if (filter != null && Sort != null)
Pipeline = new[] { filter, Skip, Limit, Sort };
else
Pipeline = filter == null ? new[] { Skip, Limit, Sort } : new[] { filter, Skip, Limit };
}
}
}

简介

作为一个数据库,基本的操作就是 CRUDMongoDBCRUD,不使用 SQL 来写,而是提供了更简单的方式。

BsonDocument方式

BsonDocument 方式,适合能熟练使用 MongoDB Shell 的开发者。MongoDB Driver 提供了完全覆盖 Shell 命令的各种方式,来处理用户的 CRUD 操作。

这种方法自由度很高,可以在不需要知道完整数据集结构的情况下,完成数据库的CRUD操作。

数据映射方式

数据映射是最常用的一种方式。准备好需要处理的数据类,直接把数据类映射到 MongoDB,并对数据集进行 CRUD 操作。

字段映射

ID字段

MongoDB 数据集中存放的数据,称之为文档(Document)。每个文档在存放时,都需要有一个ID,而这个 ID 的名称,固定叫 _id,类型是 MongoDB.Bson.ObjectId

当建立映射时,如果给出 _id 字段,则 MongoDB 会采用这个 ID 做为这个文档的 ID ,如果不给出,MongoDB 会自动添加一个 _id 字段。在使用上是完全一样的。唯一的区别是,如果映射类中不写 _id,则 MongoDB 自动添加 _id 时,会用 ObjectId 作为这个字段的数据类型。

ObjectId 是一个全局唯一的数据。

MongoDB 允许使用其它类型的数据作为 ID,例如:stringintlongGUID 等,但这就需要你自己去保证这些数据不超限并且唯一。

可以在类中修改 _id 名称为别的名称,但需要加一个描述属性 BsonIdBsonId 属性会告诉映射,topic_id 就是这个文档数据的IDMongoDB在保存时,会将这个 topic_id 转成 _id 保存到数据集中。

1
2
3
4
5
6
7
public class CollectionModel
{
[BsonId]
public ObjectId topic_id { get; set; }
public string title { get; set; }
public string content { get; set; }
}

注:在 MongoDB 数据集中,ID 字段的名称固定叫 _id。为了代码的阅读方便,可以在类中改为别的名称,但这不会影响 MongoDB 中存放的 ID 名称。

2. 特殊类型 - Decimal

MongoDB 在早期,是不支持 Decimal 的。直到 MongoDB v3.4 开始,数据库才正式支持 Decimal

所以,如果使用的是 v3.4 以后的版本,可以直接使用,而如果是以前的版本,需要用以下的方式:

1
2
[BsonRepresentation(BsonType.Double, AllowTruncation = true)]
public decimal price { get; set; }

其实就是把 Decimal 通过映射,转为 Double 存储。

类字段

添加两个类 ContactAuthor

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class Contact
{
public string mobile { get; set; }
}
public class Author
{
public string name { get; set; }
public List<Contact> contacts { get; set; }
}

public class CollectionModel
{
[BsonId]
public ObjectId topic_id { get; set; }
public string title { get; set; }
public string content { get; set; }
public int favor { get; set; }
public Author author { get; set; }
}

/*
调用 代码:
*/
private static async Task Demo()
{
CollectionModel new_item = new CollectionModel()
{
title = "Demo",
content = "Demo content",
favor = 100,
author = new Author
{
name = "WangPlus",
contacts = new List<Contact>(),
}
};

Contact contact_item1 = new Contact()
{
mobile = "13800000000",
};
Contact contact_item2 = new Contact()
{
mobile = "13811111111",
};
new_item.author.contacts.Add(contact_item1);
new_item.author.contacts.Add(contact_item2);

await _collection.InsertOneAsync(new_item);
}

文档结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{ 
"_id" : ObjectId("5ef1e635ce129908a22dfb5e"),
"title" : "Demo",
"content" : "Demo content",
"favor" : NumberInt(100),
"author" : {
"name" : "WangPlus",
"contacts" : [
{
"mobile" : "13800000000"
},
{
"mobile" : "13811111111"
}
]
}
}

枚举字段

创建一个枚举 TagEnumeration:

1
2
3
4
5
public enum TagEnumeration
{
CSharp = 1,
Python = 2,
}

加到 CollectionModel 中:

1
2
3
4
5
6
7
8
9
10
public class CollectionModel
{
[BsonId]
public ObjectId topic_id { get; set; }
public string title { get; set; }
public string content { get; set; }
public int favor { get; set; }
public Author author { get; set; }
public TagEnumeration tag { get; set; }
}

Demo代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
private static async Task Demo()
{
CollectionModel new_item = new CollectionModel()
{
title = "Demo",
content = "Demo content",
favor = 100,
author = new Author
{
name = "WangPlus",
contacts = new List<Contact>(),
},
tag = TagEnumeration.CSharp,
};

Contact contact_item1 = new Contact()
{
mobile = "13800000000",
};
Contact contact_item2 = new Contact()
{
mobile = "13811111111",
};
new_item.author.contacts.Add(contact_item1);
new_item.author.contacts.Add(contact_item2);

await _collection.InsertOneAsync(new_item);
}

保存后的文档:注意,tag 保存了枚举的值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{ 
"_id" : ObjectId("5ef1eb87cbb6b109031fcc31"),
"title" : "Demo",
"content" : "Demo content",
"favor" : NumberInt(100),
"author" : {
"name" : "WangPlus",
"contacts" : [
{
"mobile" : "13800000000"
},
{
"mobile" : "13811111111"
}
]
},
"tag" : NumberInt(1)
}

可以保存枚举的字符串。只要在 CollectionModel 中,tag 声明上加个属性:

1
2
[BsonRepresentation(BsonType.String)]
public TagEnumeration tag { get; set; }

数据会变成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{ 
"_id" : ObjectId("5ef1ec448f1d540919d15904"),
"title" : "Demo",
"content" : "Demo content",
"favor" : NumberInt(100),
"author" : {
"name" : "WangPlus",
"contacts" : [
{
"mobile" : "13800000000"
},
{
"mobile" : "13811111111"
}
]
},
"tag" : "CSharp"
}

日期字段

CollectionModel 中增加一个时间字段:

1
public DateTime post_time { get; set; }

MongoDBdatetime 存储的是 unixtimestamp ,所以默认只能是 utc0 时区的,它问题出在 C#DateTime 对时区的处理上遗留的问题,可以换成 DateTimeOffset

如果只是保存(像上边这样),或者查询时使用时间作为条件(例如查询 post_time < DateTime.Now 的数据)时,是可以使用的,不会出现问题。

但是,如果是查询结果中有时间字段,那这个字段,会被 DateTime 默认设置为 DateTimeKind.Unspecified 类型。而这个类型,是无时区信息的,输出显示时,会造成混乱。

为了避免这种情况,在进行时间字段的映射时,需要加上属性:

1
2
[BsonDateTimeOptions(Kind = DateTimeKind.Local)]
public DateTime post_time { get; set; }

这样做,会强制 DateTime 类型的字段为 DateTimeKind.Local 类型。这时候,从显示到使用就正确了。

数据集中存放的是 UTC 时间,跟我们正常的时间有8小时时差,如果我们需要按日统计,比方每天的销售额/点击量。

按年月日时分秒拆开存放

1
2
3
4
5
6
7
8
9
class Post_Time
{
public int year { get; set; }
public int month { get; set; }
public int day { get; set; }
public int hour { get; set; }
public int minute { get; set; }
public int second { get; set; }
}

MyDateTimeSerializer

1
2
3
4
5
6
7
8
9
10
11
12
13
public class MyDateTimeSerializer : DateTimeSerializer
{
public override DateTime Deserialize(BsonDeserializationContext context, BsonDeserializationArgs args)
{
var obj = base.Deserialize(context, args);
return new DateTime(obj.Ticks, DateTimeKind.Unspecified);
}
public override void Serialize(BsonSerializationContext context, BsonSerializationArgs args, DateTime value)
{
var utcValue = new DateTime(value.Ticks, DateTimeKind.Utc);
base.Serialize(context, args, utcValue);
}
}

注意:使用这个方法,一定不要添加时间的属性 [BsonDateTimeOptions(Kind = DateTimeKind.Local)]

对某个特定映射的特定字段使用,比方只对 CollectionModelpost_time 字段来使用,可以这么写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[BsonSerializer(typeof(MyDateTimeSerializer))]
public DateTime post_time { get; set; }

//或者全局使用:

BsonSerializer.RegisterSerializer(typeof(DateTime), new MongoDBDateTimeSerializer());

// BsonSerializer是 MongoDB.Driver 的全局对象,可以放到使用数据库前的任何地方。例如在Demo中,放在Main里:
static async Task Main(string[] args)
{
BsonSerializer.RegisterSerializer(typeof(DateTime), new MyDateTimeSerializer());

await Demo();
Console.ReadKey();
}

Dictionary字段

数据声明很简单:

public Dictionary<string, int> extra_info { get; set; }

MongoDB 定义了三种保存属性:DocumentArrayOfDocumentsArrayOfArrays,默认是 Document

属性写法是这样的:

1
2
[BsonDictionaryOptions(DictionaryRepresentation.ArrayOfDocuments)]
public Dictionary<string, int> extra_info { get; set; }

这三种属性下,保存在数据集中的数据结构有区别。多数情况用 DictionaryRepresentation.ArrayOfDocuments

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// DictionaryRepresentation.Document:
{
"extra_info" : {
"type" : NumberInt(1),
"mode" : NumberInt(2)
}
}


// DictionaryRepresentation.ArrayOfDocuments:
{
"extra_info" : [
{
"k" : "type",
"v" : NumberInt(1)
},
{
"k" : "mode",
"v" : NumberInt(2)
}
]
}

// DictionaryRepresentation.ArrayOfArrays:
{
"extra_info" : [
[
"type",
NumberInt(1)
],
[
"mode",
NumberInt(2)
]
]
}

这三种方式,从数据保存上并没有什么区别,但从查询来讲,如果这个字段需要进行查询,那三种方式区别很大。

如果采用 BsonDocument 方式查询,DictionaryRepresentation.Document 无疑是写着最方便的。

如果用 Builder 方式查询,DictionaryRepresentation.ArrayOfDocuments 是最容易写的。

DictionaryRepresentation.ArrayOfArrays 就算了。数组套数组,查询条件写死人。

BsonElement属性

用来改数据集中的字段名称。

1
2
[BsonElement("pt")]
public DateTime post_time { get; set; }

在不加 BsonElement 的情况下,通过数据映射写到数据集中的文档,字段名就是变量名,上面这个例子,字段名就是 post_time

加上 BsonElement 后,数据集中的字段名会变为 pt

BsonDefaultValue属性

设置字段的默认值。
当写入的时候,如果映射中不传入值,则数据库会把这个默认值存到数据集中。

1
2
[BsonDefaultValue("This is a default title")]
public string title { get; set; }

BsonRepresentation属性

用来在映射类中的数据类型和数据集中的数据类型做转换。

1
2
[BsonRepresentation(BsonType.String)]
public int favor { get; set; }

表示,在映射类中,favor 字段是 int 类型的,而存到数据集中,会保存为 string 类型。

前边 Decimal 转换和枚举转换,就是用的这个属性。

BsonIgnore属性

用来忽略某些字段。
忽略的意思是:映射类中某些字段,不希望被保存到数据集中。

1
2
[BsonIgnore]
public string ignore_string { get; set; }

这样,在保存数据时,字段 ignore_string 就不会被保存到数据集中。

参考:

MongoDB via Dotnet Core数据映射详解

默认是没有 categoriestags 的:

1
2
hexo new page "tags"
hexo new page "categories"

编辑 /tags/index.md

添加:

1
2
type: tags
layout: tags

/categories/index.md

添加:

1
2
type: categories
layout: categories

  • 一月 January,缩写 Jan
  • 二月 February,缩写 Feb
  • 三月 March,缩写 Mar
  • 四月 April,缩写 Apr
  • 五月 May,缩写 May
  • 六月 June,缩写 Jun
  • 七月 July,缩写 Jul
  • 八月 August,缩写 Aug
  • 九月 September,缩写 Sep/Sept
  • 十月 October,缩写 Oct
  • 十一月 November,缩写 Nov
  • 十二月 December,缩写 Dec

常见文件注解

linux系统文件通常在 /var/log

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 系统启动后的信息和错误日志
/var/log/message

# 与安全相关的日志信息
/var/log/secure

# 与邮件相关的日志信息
/var/log/maillog

# 与定时任务相关的日志信息
/var/log/cron

# 与UUCP和news设备相关的日志信息
/var/log/spooler

# 守护进程启动和停止相关的日志消息
/var/log/boot.log

# 永久记录每个用户登录、注销及系统的启动、停机的事件
/var/log/wtmp

# 记录当前正在登录系统的用户信息
/var/run/utmp

# 记录失败的登录尝试信息
/var/log/btmp

历史记录

1
2
3
4
5
6
7
8
# 查看重启的命令
last | grep reboot

# 历史操作
history

# 立即清空当前所有的历史记录
history -c

最近重启记录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 查看所有重启日志信息
last reboot

# 查看最近的一条
last reboot | head -1

last -x|grep shutdown | head -1

# -x:显示系统关机和运行等级改变信息
last
last -x
last -x reboot
last -x shutdown

# 开机时间
uptime -s

参考:

linux系统重启 查看日志及历史记录

ObjectId

ObjectId 是一个12字节 BSON 类型数据,有以下格式:

  • 前4个字节表示时间戳
  • 接下来的3个字节是机器标识码
  • 紧接的两个字节由进程id组成(PID)
  • 最后三个字节是随机数。

MongoDB 中存储的文档必须有一个 _id 键。这个键的值可以是任何类型的,默认是个 ObjectId 对象。

在一个集合里面,每个文档都有唯一的 _id 值,来确保集合里面每个文档都能被唯一标识。

MongoDB 采用 ObjectId,而不是其他比较常规的做法(比如自动增加的主键)的主要原因,因为在多个服务器上同步自动增加主键值既费力还费时。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 1.创建新的ObjectId
newObjectId = ObjectId()
# 输出
ObjectId("5f6d62d8b7424d4010318ac6")

# 也可以使用生成的id来取代MongoDB自动生成的ObjectId:
myObjectId = ObjectId("5f6d62d8b7424d4010318ac6")

# 2.创建文档的时间戳
# 由于 ObjectId 中存储了4个字节的时间戳,所以你不需要为你的文档保存时间戳字段,
# 可以通过 getTimestamp 函数来获取文档的创建时间:
ObjectId("5f6d62d8b7424d4010318ac6").getTimestamp()
# 输出
ISODate("2020-09-25T03:24:08Z")

# 3.ObjectId 转换为字符串
new ObjectId().str
# 输出
5f6d6374b7424d4010318ac7

文档关系

MongoDB 中的关系分为:嵌入式关系引用式关系

嵌入式关系

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"_id":ObjectId("52ffc33cd85242f436000001"),
"phone": "123345345",
"name": "test",
"address": [
{
"pincode": 123456,
"city": "beijing",
"state": "California"
},
{
"pincode": 456789,
"city": "shanghai",
"state": "Illinois"
}]
}

查询:

db.users.findOne({"name":"test"},{"address":1})

这种数据结构的缺点是,如果用户和用户地址在不断增加,数据量不断变大,会影响读写性能。

引用式关系

1
2
3
4
5
6
7
8
9
10
{
"_id":ObjectId("52ffc33cd85242f436000001"),
"contact": "987654321",
"dob": "01-01-1991",
"name": "test",
"address_ids": [
ObjectId("52ffc4a5d85242602e000000"),
ObjectId("52ffc4a5d85242602e000001")
]
}

查询:

1
2
3
4
5
6
var result = db.users.findOne({"name":"test"},{"address_ids":1})
var addresses = db.address.find({"_id":{"$in":result["address_ids"]}})

# 或
var result = db.users.find({"name":"test"},{"address_ids":1})
var addresses = db.address.find({"_id":{"$in":result[0]["address_ids"]}})

注意:find 返回的数据类型是数组,findOne 返回的数据类型是对象。

引用关系

MongoDB 引用有两种:

  • 手动引用(Manual References)
  • DBRefs

DBRefs

1
2
3
4
# $ref:集合名称
# $id:引用的id
# $db:数据库名称,可选参数
{ $ref : , $id : , $db : }

实例:

1
2
3
4
5
6
7
8
9
10
{
"_id":ObjectId("53402597d852426020000002"),
"address": {
"$ref": "address_home",
"$id": ObjectId("534009e4d852427820000002"),
"$db": "runoob"},
"contact": "987654321",
"dob": "01-01-1991",
"name": "test1"
}

查询:

1
2
3
4
5
6
7
var user = db.users.findOne({"name":"test1"})
var dbRef = user.address

db[dbRef.$ref].findOne({"_id":(dbRef.$id)})

# MongoDB4.0 版本写法
db[dbRef.$ref].findOne({"_id":ObjectId(dbRef.$id)})

覆盖索引查询

官方的 MongoDB 的文档中说明,覆盖查询是以下的查询:

  • 所有的查询字段是索引的一部分
  • 所有的查询返回字段在同一个索引中

由于所有出现在查询中的字段是索引的一部分, MongoDB 无需在整个数据文档中检索匹配查询条件和返回使用相同索引的查询结果。
因为索引存在于 RAM 中,从索引中获取数据比通过扫描文档读取数据要快得多。

使用覆盖索引查询

实例:

1
2
3
4
5
6
7
8
{
"_id": ObjectId("53402597d852426020000002"),
"contact": "987654321",
"dob": "01-01-1991",
"gender": "M",
"name": "test1",
"user_name": "tombenzamin"
}

users 集合中创建联合索引,字段为 genderuser_name :

db.users.createIndex({gender:1,user_name:1})

该索引会覆盖以下查询:

db.users.find({gender:"M"},{user_name:1,_id:0})

上述查询,MongoDB 的不会去数据库文件中查找。它会从索引中提取数据,这是非常快速的数据查询。

由于索引中不包括 _id 字段,_id 在查询中会默认返回,我们可以在 MongoDB 的查询结果集中排除它。

下面的实例没有排除 _id,查询就不会被覆盖:

db.users.find({gender:"M"},{user_name:1})

最后,如果是以下的查询,不能使用覆盖索引查询:

  • 所有索引字段是一个数组
  • 所有索引字段是一个子文档

查询分析

MongoDB 查询分析常用函数有:explain()hint()

explain()

explain 操作提供了查询信息,使用索引及查询统计等。有利于我们对索引的优化。

1
2
3
4
5
# users 集合中创建 gender 和 user_name 的索引:
db.users.createIndex({gender:1,user_name:1})

# 在查询语句中使用 explain :
db.users.find({gender:"M"},{user_name:1,_id:0}).explain()

explain() 查询返回:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{
"cursor" : "BtreeCursor gender_1_user_name_1",
"isMultiKey" : false,
"n" : 1,
"nscannedObjects" : 0,
"nscanned" : 1,
"nscannedObjectsAllPlans" : 0,
"nscannedAllPlans" : 1,
"scanAndOrder" : false,
"indexOnly" : true,
"nYields" : 0,
"nChunkSkips" : 0,
"millis" : 0,
"indexBounds" : {
"gender" : [
[
"M",
"M"
]
],
"user_name" : [
[
{
"$minElement" : 1
},
{
"$maxElement" : 1
}
]
]
}
}

结果集的字段:

  • indexOnly: 字段为 true ,表示我们使用了索引。
  • cursor:因为这个查询使用了索引,MongoDB 中索引存储在B树结构中,所以这是也使用了 BtreeCursor 类型的游标。如果没有使用索引,游标的类型是 BasicCursor。这个键还会给出你所使用的索引的名称,你通过这个名称可以查看当前数据库下的 system.indexes 集合(系统自动创建,由于存储索引信息,这个稍微会提到)来得到索引的详细信息。
  • n:当前查询返回的文档数量。
  • nscanned/nscannedObjects:表明当前这次查询一共扫描了集合中多少个文档,我们的目的是,让这个数值和返回文档的数量越接近越好。
  • millis:当前查询所需时间,毫秒数。
  • indexBounds:当前查询具体使用的索引。

hint()

hint() 来强制 MongoDB 使用一个指定的索引。

1
2
3
4
db.users.find({gender:"M"},{user_name:1,_id:0}).hint({gender:1,user_name:1})

# 可以使用 explain() 函数来分析以上查询:
db.users.find({gender:"M"},{user_name:1,_id:0}).hint({gender:1,user_name:1}).explain()

原子操作

MongoDB 提供了许多原子操作,比如文档的保存,修改,删除等,都是原子操作。

所谓原子操作就是要么这个文档保存到 MongoDB,要么没有保存到 MongoDB,不会出现查询到的文档没有保存完整的情况。

db.collection.findAndModify() 方法来判断书籍是否可结算并更新新的结算信息。

1
2
3
4
5
6
7
8
9
10
db.books.findAndModify ( {
query: {
_id: 123456789,
available: { $gt: 0 }
},
update: {
$inc: { available: -1 },
$push: { checkout: { by: "abc", date: new Date() } }
}
} )

原子操作常用命令

$set

用来指定一个键并更新键值,若键不存在并创建。

{ $set : { field : value } }

$unset

用来删除一个键。

{ $unset : { field : 1} }

$inc

$inc可以对文档的某个值为数字型(只能为满足要求的数字)的键进行增减的操作。

{ $inc : { field : value } }

$push

把value追加到field里面去,field一定要是数组类型才行,如果field不存在,会新增一个数组类型加进去。

{ $push : { field : value } }

$pushAll

$push,只是一次可以追加多个值到一个数组字段内。

{ $pushAll : { field : value_array } }

$pull

从数组field内删除一个等于value值。

{ $pull : { field : _value } }

$addToSet

增加一个值到数组内,而且只有当这个值不在数组内才增加。

$pop

删除数组的第一个或最后一个元素

{ $pop : { field : 1 } }

$rename

修改字段名称

{ $rename : { old_field_name : new_field_name } }

$bit

位操作,integer类型

{$bit : { field : {and : 5}}}

偏移操作符

1
2
3
4
5
6
t.find() { "_id" : ObjectId("4b97e62bf1d8c7152c9ccb74"), "title" : "ABC", "comments" : [ { "by" : "joe", "votes" : 3 }, { "by" : "jane", "votes" : 7 } ] }

# 重点
t.update( {'comments.by':'joe'}, {$inc:{'comments.$.votes':1}}, false, true )

t.find() { "_id" : ObjectId("4b97e62bf1d8c7152c9ccb74"), "title" : "ABC", "comments" : [ { "by" : "joe", "votes" : 4 }, { "by" : "jane", "votes" : 7 } ] }

高级索引

实例 文档集合(users)包含了 address 子文档和 tags 数组:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"address": {
"city": "Los Angeles",
"state": "California",
"pincode": "123"
},
"tags": [
"music",
"cricket",
"blogs"
],
"name": "Tom Benzamin"
}

索引数组字段

在数组中创建索引,需要对数组中的每个字段依次建立索引。所以在我们为数组 tags 创建索引时,会为 music、cricket、blogs 三个值建立单独的索引。

1
2
3
4
5
6
7
8
9
10
# 创建数组索引:

db.users.createIndex({"tags":1})

# 创建索引后,我们可以这样检索集合的 tags 字段:
db.users.find({tags:"cricket"})

# 为了验证我们使用使用了索引,可以使用 explain 命令:
# 执行结果中会显示 "cursor" : "BtreeCursor tags_1" ,则表示已经使用了索引。
db.users.find({tags:"cricket"}).explain()

索引子文档字段

1
2
3
4
5
6
7
8
9
10
11
# 为子文档的三个字段创建索引,命令如下:
db.users.createIndex({"address.city":1,"address.state":1,"address.pincode":1})

# 一旦创建索引,我们可以使用子文档的字段来检索数据:
db.users.find({"address.city":"Los Angeles"})

# 查询表达不一定遵循指定的索引的顺序,mongodb 会自动优化。所以上面创建的索引将支持以下查询:
db.users.find({"address.state":"California","address.city":"Los Angeles"})

# 同样支持以下查询:
db.users.find({"address.city":"Los Angeles","address.state":"California","address.pincode":"123"})

索引限制

额外开销

每个索引占据一定的存储空间,在进行插入,更新和删除操作时也需要对索引进行操作。所以,如果你很少对集合进行读取操作,建议不使用索引。

内存(RAM)使用

由于索引是存储在内存( RAM )中,你应该确保该索引的大小不超过内存的限制。

如果索引的大小大于内存的限制,MongoDB 会删除一些索引,这将导致性能下降。

查询限制

索引不能被以下的查询使用:

正则表达式及非操作符,如 $nin, $not, 等。
算术运算符,如 $mod, 等。
$where 子句
所以,检测你的语句是否使用索引是一个好的习惯,可以用 explain 来查看。

索引键限制

从2.6版本开始,如果现有的索引字段的值超过索引键的限制,MongoDB 中不会创建索引。

插入文档超过索引键限制

如果文档的索引字段值超过了索引键的限制,MongoDB 不会将任何文档转换成索引的集合。与 mongorestoremongoimport 工具类似。

最大范围

集合中索引不能超过64个
索引名的长度不能超过128个字符
一个复合索引最多可以有31个字段

Map Reduce

Map-Reduce 是一种计算模型,简单的说就是将大批量的工作(数据)分解(MAP)执行,然后再将结果合并成最终结果(REDUCE)。

MongoDB 提供的 Map-Reduce 非常灵活,对于大规模数据分析也相当实用。

MapReduce 命令基本语法

1
2
3
4
5
6
7
8
9
10
db.collection.mapReduce(
function() {emit(key,value);}, //map 函数
function(key,values) {return reduceFunction}, //reduce 函数
{
out: collection,
query: document,
sort: document,
limit: number
}
)

使用 MapReduce 要实现两个函数 Map 函数和 Reduce 函数,Map 函数调用 emit(key, value), 遍历 collection 中所有的记录, 将 keyvalue 传递给 Reduce 函数进行处理。

Map 函数必须调用 emit(key, value) 返回键值对。

参数说明:

  • map :映射函数 (生成键值对序列,作为 reduce 函数参数)。
  • reduce 统计函数,reduce 函数的任务就是将 key-values 变成 key-value ,也就是把 values 数组变成一个单一的值 value。。
  • out 统计结果存放集合 (不指定则使用临时集合,在客户端断开后自动删除)。
  • query 一个筛选条件,只有满足条件的文档才会调用 map 函数。( query,limitsort可以随意组合)
  • sortlimit 结合的 sort 排序参数(也是在发往 map 函数前给文档排序),可以优化分组机制
  • limit 发往 map 函数的文档数量的上限(要是没有 limit,单独使用 sort 的用处不大)

以下实例在集合 orders 中查找 status:"A" 的数据,并根据 cust_id 来分组,并计算 amount 的总和。

map-reduce.bakedsvg.svg

使用MapReduce

posts 集合中使用 mapReduce 函数来选取已发布的文章(status:”active”),并通过 user_name 分组,计算每个用户的文章数:

1
2
3
4
5
6
7
8
9
10
11
12
13
db.posts.mapReduce( 
function() { emit(this.user_name,1); },
function(key, values) {return Array.sum(values)},
{
query:{status:"active"},
out:"post_total"
}
)
db.post_total.find()

# 输出
{ "_id" : "runoob", "value" : 1 }
{ "_id" : "mark", "value" : 4 }

全文检索

全文检索对每一个词建立一个索引,指明该词在文章中出现的次数和位置,当用户查询时,检索程序就根据事先建立的索引进行查找,并将查找的结果反馈给用户的检索方式。MongoDB 从 2.4 版本开始支持全文检索,MongoDB 从3.2 版本以后添加了对中文索引的支持。

启用全文检索

MongoDB 在 2.6 版本以后是默认开启全文检索的,如果你使用之前的版本,你需要使用以下代码来启用全文检索:

1
2
3
4
db.adminCommand({setParameter:true,textSearchEnabled:true})

# 或者使用命令:
mongod --setParameter textSearchEnabled=true

创建全文索引

实例:posts 集合的文档数据,包含了文章内容(post_text)及标签(tags):

1
2
3
4
5
6
7
{
"post_text": "enjoy the mongodb articles on Runoob",
"tags": [
"mongodb",
"runoob"
]
}

对 post_text 字段建立全文索引,这样我们可以搜索文章内的内容:

db.posts.createIndex({post_text:"text"})

使用全文索引

1
2
3
4
5
6
7
8
db.posts.find({$text:{$search:"runoob"}})

# 结果:
{
"_id" : ObjectId("53493d14d852429c10000002"),
"post_text" : "enjoy the mongodb articles on Runoob",
"tags" : [ "mongodb", "runoob" ]
}

旧版本的 MongoDB,可以使用以下命令:

db.posts.runCommand("text",{search:"runoob"})

使用全文索引可以提高搜索效率。

删除全文索引

1
2
3
4
5
# 删除已存在的全文索引,可以使用 find 命令查找索引名:
db.posts.getIndexes()

# 执行以下命令来删除索引:
db.posts.dropIndex("post_text_text")

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 创建物理卷
pvcreate /dev/vdb

# 扩展卷组
vgextend centos /dev/vdb

# 扩展逻辑卷大小
lvextend -L +500G /dev/mapper/centos-root
# 或 将卷组 100% 分配到 逻辑卷unicomvol
lvextend -l 100%VG /dev/mapper/centos-root

# 确认文件系统是xfs
cat /etc/fstab | grep centos-root

# xfs_growfs: 调整一个 xfs 文件系统大小(只能扩展)
xfs_growfs /dev/mapper/centos-root

# 可选,resize2fs
# resize2fs -p /dev/mapper/centos-root

# 查看
df -hT

xfs 相关:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
xfs_admin: 调整 xfs 文件系统的各种参数
xfs_copy: 拷贝 xfs 文件系统的内容到一个或多个目标系统(并行方式) 
xfs_db: 调试或检测 xfs 文件系统(查看文件系统碎片等) 
xfs_check: 检测 xfs 文件系统的完整性 
xfs_bmap: 查看一个文件的块映射 
xfs_repair: 尝试修复受损的 xfs 文件系统 
xfs_fsr: 碎片整理 
xfs_quota: 管理 xfs 文件系统的磁盘配额 
xfs_metadump: 将 xfs 文件系统的元数据 (metadata) 拷贝到一个文件中 
xfs_mdrestore: 从一个文件中将元数据 (metadata) 恢复到 xfs 文件系统 
xfs_growfs: 调整一个 xfs 文件系统大小(只能扩展) 
xfs_freeze    暂停(-f)和恢复(-u)xfs 文件系统
xfs_logprint: 打印xfs文件系统的日志 
xfs_mkfile: 创建xfs文件系统 
xfs_info: 查询文件系统详细信息 
xfs_ncheck: generate pathnames from i-numbers for XFS 
xfs_rtcp: XFS实时拷贝命令 
xfs_io: 调试xfs I/O路径

resize2fs 相关

此命令的适用范围:RedHat、RHEL、Ubuntu、CentOS、SUSE、openSUSE、Fedora

调整 ext2\ext3\ext4 文件系统的大小,它可以放大或者缩小没有挂载的文件系统的大小。如果文件系统已经挂载,它可以扩大文件系统的大小,前提是内核支持在线调整大小。

size 参数指定所请求的文件系统的新大小。如果没有指定任何单元,那么 size 参数的单位应该是文件系统的文件系统块大小。size 参数可以由下列单位编号之一后缀:sKMG,分别用于512字节扇区、千字节、兆字节或千兆字节。文件系统的大小可能永远不会大于分区的大小。如果未指定 Size 参数,则它将默认为分区的大小。

resize2fs 程序不操作分区的大小。如果希望扩大文件系统,必须首先确保可以扩展基础分区的大小。如果您使用逻辑卷管理器 LVM(8) ,可以使用 fdisk(8) 删除分区并以更大的大小重新创建它,或者使用 lvexport(8) 。在重新创建分区时,请确保使用与以前相同的启动磁盘圆柱来创建分区!否则,调整大小操作肯定无法工作,您可能会丢失整个文件系统。运行 fdisk(8) 后,运行 resize2fs 来调整 ext 2文件系统的大小,以使用新扩大的分区中的所有空间。

如果希望缩小 ext2 分区,请首先使用 resize2fs 缩小文件系统的大小。然后可以使用 fdisk(8) 缩小分区的大小。缩小分区大小时,请确保不使其小于 ext2 文件系统的新大小。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 语法相关
resize2fs [选项] device [size]
resize2fs [ -fFpPM ] [ -d debug-flags ] [ -S RAID-stride ] device [ size ]

# 选项列表
-d debug-flags
打开各种resize2fs调试特性,如果它们已经编译成二进制文件的话。调试标志应该通过从以下列表中添加所需功能的数量来计算:
2,调试块重定位。
4,调试iNode重定位。
8,调试移动inode表。
-f 强制执行,覆盖一些通常强制执行的安全检查。
-F 执行之前,刷新文件系统的缓冲区
-M 将文件系统缩小到最小值
-p 显示已经完成任务的百分比
-P 显示文件系统的最小值
-S RAID-stride

resize2fs 程序将启发式地确定在创建文件系统时指定的 RAID 步长。此选项允许用户显式地指定 RAID 步长设置,以便由 resize2fs 代替。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# 查看防火状态
# CentOS7
systemctl status firewalld
# Red hat
service iptables status

# 暂时关闭防火墙
# CentOS7
systemctl stop firewalld
# Red hat
service iptables stop

# 重启防火墙
# CentOS7
systemctl enable firewalld
# Red hat
service iptables restart

# 永久关闭防火墙
# CentOS7
systemctl disable firewalld
# Red hat
chkconfig iptables off