1 | ARGF: 从ARGV读取文件的流 |
1 | #实例方法 |
1 | #类方法 |
1 | #实例方法 |
1 | #类方法 |
1 | #实例方法 |
1 |
详细计划见原文链接:https://github.com/crystal-lang/crystal/wiki/Roadmap
]]>原文链接 :https://crystal-lang.org/2018/01/08/top-5-reasons-for-ruby-ists-to-use-crystal.html
笔者喜欢Crystal的理由:简单、超高性能、布署方便、开源有生命力,与笔者放荡不羁的性格相匹配。。。
]]>实行静态编译 (Crystal默认不是采用静态编译的,这样编译出来的可执行文件需要依赖系统环境的动态库,不同环境的系统运行时可能出现依赖问题。这样做的好处是编译后的文件比较小。)
静态连接编译后的文件自带依赖库,可以放到任意主机上直接运行。
编译方法: 加参数 –link-flags -static ,下面例子:1
2
crystal build src/myproj.cr --release --link-flags -static
有一定的类型推断能力,在堆码的过程中不一定需要显示的指定数据类型。自动GC。
本手册意在讲解Crystal语言除API手册之外的各个方面,帮助读者快速使用Crystal语言。文档结构基本与官方一致,但并非完全相同。
本站专注Crystal相关信息,并持续关注这门语言的发展动向。截至目前为止尚未发布正式版,预计在2018年发布首个1.x版本。
官网地址: https://crystal-lang.org/
API手册: https://crystal-lang.org/api/0.24.1/
Debian分支的Linux可以使用官方的仓库来安装。
1 | curl https://dist.crystal-lang.org/apt/setup.sh | sudo bash |
然后1
2
3apt-key adv --keyserver keys.gnupg.net --recv-keys 09617FD37CC06B54
echo "deb https://dist.crystal-lang.org/apt crystal main" > /etc/apt/sources.list.d/crystal.list
apt-get update
1 | sudo apt-get install crystal |
有新的版本发布时,使用如下命令更新1
2sudo apt-get update
sudo apt-get install crystal
可以使用官方的仓库安装
1 | curl https://dist.crystal-lang.org/rpm/setup.sh | sudo bash |
1 | sudo yum install crystal |
有新的版本发布时可以执行1
sudo yum update crystal
社区的仓库中已经包含了Crystal的编译器
1 | sudo pacman -S crystal |
Gentoo的版本仓库已经包含了Crystal的编译器
你可以先查看一下设置,运行1
2
3
4
5
6
7
8
9
10# equery u dev-lang/crystal
[ Legend : U - final flag setting for installation]
[ : I - package is installed with flag ]
[ Colors : set, unset ]
* Found these USE flags for dev-lang/crystal-0.18.7:
U I
- - doc : Add extra documentation (API, Javadoc, etc). It is recommended to enable per package instead of globally
- - examples : Install examples, usually source code
+ + xml : Use the dev-libs/libxml2 library to enable Crystal xml module
+ - yaml : Use the dev-libs/libyaml library to enable Crystal yaml module
1 | su - |
1 | brew update |
如果你想对Crystal捐献代码,需要同时安装LLVM, 替换上面代码的最后一行为1
brew install crystal-lang --with-llvm
关于在 OSX 10.11 (El Capitan)的一些问题
如果报如下的错误1
ld: library not found for -levent
你需要重装命令行工具,并选择默认的工具链1
2$ xcode-select --install
$ xcode-select --switch /Library/Developer/CommandLineTools
可以在Linux发行版中使用Linuxbrew安装Crystal1
2brew update
brew install crystal-lang
如果你想对Crystal捐献代码,需要同时安装LLVM, 替换上面代码的最后一行为1
brew install crystal-lang --with-llvm
Crystal目前还不支持windows系统, 如果你使用的是Windows10 你可以尝试使用Bash on Ubuntu on Windows
(win10红石版的一个Bash环境)来安装, 安装方式跟Ubuntu的安装方式一致,但可能还是会有些问题(这是一个实验性的尝试)
1 | curl -sSL https://dist.crystal-lang.org/apt/setup.sh | sudo bash |
需要安装C的编译器(cc) 和连接器(ld )1
sudo apt-get install clang binutils
1 | sudo apt-get install crystal |
1 | sudo apt-get update |
如果你不想使用前面的任何一种安装方法, 你可以下载Crystal的单文件版本。最新版本的Github地址是 https://github.com/crystal-lang/crystal/releases
下载对应平台的版本,在包中有一个 bin/crystal的可执行文件, 创建一个符号链接到你的系统Path环境变量目录中
ln -s [full path to bin/crystal] /usr/local/bin/crystal
这样就可以直接输入 ctystal 命令来使用了。
If you want to contribute then you might want to install Crystal from sources.
Install the latest Crystal release. To compile Crystal, you need Crystal :).
Make sure a supported LLVM version is present in the path. Currently, Crystal supports LLVM 3.8, 3.9 and 4.0. When possible, use the latest one. If you are using Mac and the Homebrew formula, this will be automatically configured for you if you install Crystal adding –with-llvm flag.
Make sure to install all the required libraries. You might also want to read the contributing guide.
Clone the repository:
git clone https://github.com/crystal-lang/crystal.git
Run make to build your own version of the compiler
Run make spec to ensure all specs pass, and you’ve installed everything correctly.
Use bin/crystal to run your crystal files
If you would like more information about the new bin/crystal, check out the using the compiler documentation.
Note: The actual binary is built in to .build/crystal, but the bin/crystal wrapper script is what you should use to run crystal.
]]>要编译和运行Crystal程序, 可以在crystal指令后面跟程序文件名1
crystal some_program.cr
crystal程序文件以.cr结尾 。
也可以带上run命令来执行1
$ crystal run some_program.cr
使用build命令来创建1
crystal build some_program.cr
这样就创建了一个 some_program的可执行文件 ,可以这样调用1
./some_program
注意: 这样创建的可执行程序还没有最优化,要创建优化的程序需要带上 –release参数1
$ crystal build some_program.cr --release
请确保正式释放版本 或者跑Benchmark时都带上 –release参数。
默认不带–release的原因在于,在没有优化编译的情况下能加快编译的速度 这样你就可以把crystal命令行当成程序解释器来玩耍。
注:有个icr的项目 ,可以提供crystal语言的命令行交互环境。
使用 init命令可以创建一个带有标准目录结构的Crystal项目1
2
3
4
5
6
7
8
9
10
11
12$ crystal init lib my_cool_lib
create my_cool_lib/.gitignore
create my_cool_lib/.editorconfig
create my_cool_lib/LICENSE
create my_cool_lib/README.md
create my_cool_lib/.travis.yml
create my_cool_lib/shard.yml
create my_cool_lib/src/my_cool_lib.cr
create my_cool_lib/src/my_cool_lib/version.cr
create my_cool_lib/spec/spec_helper.cr
create my_cool_lib/spec/my_cool_lib_spec.cr
Initialized empty Git repository in ~/my_cool_lib/.git/
可以使用不带任何参数的crystal指令来查看1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16$ crystal
Usage: crystal [command] [switches] [program file] [--] [arguments]
Command:
init # 生成一个新项目
build # 创建一个可执行文件
deps # 安装项目依赖
docs # 生成文档
env # 打印Crystal的环境信息
eval # 从标准输入或参数中运行Crystal代码
play # 启动Crystal的Playground服务
run (默认) # 构建并运行项目
spec # 创建并运行 specs (在spec目录)
tool run a tool
help, --help, -h # 显示帮助
version, --version, -v # 显示版本号
要查看具体命令的参数, 可以在指令后面跟上 –help参数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$ crystal build --help
Usage: crystal build [options] [programfile] [--] [arguments]
Options:
--cross-compile # 交叉编译
-d, --debug # Add full symbolic debug info
--no-debug # 不输出Debug信息
-D FLAG, --define FLAG # 定义一个编译时 flag
--emit [asm|llvm-bc|llvm-ir|obj] # 输出编译器信息,多个类别可以使用逗号分隔
-f text|json, --format text|json # 格式化输出 text(默认) 或json
--error-trace # 显示所有错误追踪信息
-h, --help # Show this message
--ll # Dump ll to Crystal's cache directory
--link-flags FLAGS # Additional flags to pass to the linker,-static表示静态链接
--mcpu CPU # 指定Cpu类型
--mattr CPU # Target specific features
--no-color # Disable colored output
--no-codegen # Don't do code generation
-o # 指定编译输出的文件名(Output filename )
--prelude # Use given file as prelude
--release # 以正式发行版的模式编译(Compile in release mode )
-s, --stats # Enable statistics output
-p, --progress # Enable progress output
-t, --time # Enable execution time output
--single-module # Generate a single LLVM module
--threads # 限制使用的最大线程数(Maximum number of threads to use )
--target TRIPLE # Target triple
--verbose # Display executed commands
--static # Link statically
--stdin-filename # Source file name to be read from STDIN
完
]]>在Crystal中典型的”hello world” 例子长这样1
puts "Hello world!"
从这个例子可以看出程序运行的入口就是自身 ,没有必要创建一个类似main这样的函数。
一个简单有趣的Http服务器1
2
3
4
5
6
7
8
9require "http/server"
server = HTTP::Server.new(8080) do |context|
context.response.content_type = "text/plain"
context.response.print "Hello world! The time is #{Time.now}"
end
puts "Listening on http://127.0.0.1:8080"
server.listen
看完语法教程之后你对上面的代码会有所领悟, 但是现在依然可以学到点东西
可以通过require来召唤写在其它文件中的代码
1 | require "http/server" |
可以不指定本地变量的类型
1 | server = HTTP::Server.new ... |
你通过调用对象的方法或发送消息来编写程序
1 | HTTP::Server.new(8080) ... |
在函数的世界里,你可以方便的调用和复用代码块
1 | HTTP::Server.new(8080) do |context| |
你可以使用变量插值来方便的定义字符串。
1 | "Hello world! The time is #{Time.now}" |
本节内容较多,老司机打算用最简洁的语言带你快速飙车,看完保证不翻车^_^
友情提示:先装好icr 以方便验证及练习 https://github.com/crystal-community/icr
使用#做单行注释,目前仅支持单行。
1 | # This is a comment |
代表空值, 类似其它语言中的null
仅有两值 true, false
分别有4个无符号整型 和4个整型
Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64 , 附表1
2
3
4
5
6
7
8
9TypeLengthMinimum ValueMaximum Value
Int88-128127
Int1616−32,76832,767
Int3232−2,147,483,6482,147,483,647
Int6464−263263 - 1
UInt880255
UInt1616065,535
UInt323204,294,967,295
UInt64640264 - 1
在不带后缀的情况下,整型匹配的范围是Int32 ~ Int64 和 UInt64 ,如下:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
171
1_i8
1_i16
1_i32
1_i64
1_u8
1_u16
1_u32
1_u64
+10
-20
2147483648
9223372036854775808
后缀前面的 是可选的, 能使代码更具可读性:1
1_000_000 # 好过于 1000000
二进制数以0b开头 :1
0b1101 # == 13
八进制以0o开头:1
0 # == 83
十六进制以0x开头1
20xFE012D # == 16646445
0xfe012d # == 16646445
有两种 Float32 和 Float64, 没有符号限制, 可以+/- ,跟整型一样可以加后缀 没有后缀默认为 Float641
2
3
4
5
6
7
8
9
101.0
1.0_f32
1_f32
1e10
1.5e10
1.5e-7
+1.3
-0.5
一个字符代表一个32位的Unicode code point, 使用单引号创建1
2
3
4
5'a'
'z'
'0'
'_'
'あ'
反斜杠是一个特殊的字符, 可以用于转义或正则 也可以用于表达一个16进制整数在unicode codepoint中的字符
A backslash denotes a special character, which can either be a named escape sequence or a numerical representation of a unicode codepoint.
Available escape sequences:1
2
3
4
5
6
7
8
9
10
11'\''
'\\'
'\b'
'\e'
'\f'
'\n'
'\r'
'\t'
'\v'
'\uNNNN'
'\u{NNNN...}'
(此处翻车)A backslash followed by a u denotes a unicode codepoint. It can either be followed by exactly four hexadecimal characters representing the unicode bytes (\u0000 to \uFFFF) or a number of one to six hexadecimal characters wrapped in curly braces (\u{0} to \u{10FFFF}.1
2
3'\u0041'
'\u{41}'
'\u{1F52E}'
一个字符串代表一个不可更改的UTF8字符序列,使用双引号包含。
A backslash denotes a special character inside a string, which can either be a named escape sequence or a numerical representation of a unicode codepoint.
1 | "\"" # double quote |
A backslash followed by at most three digits denotes a code point written in octal:1
2
3
4"\101" # => "A"
"\123" # => "S"
"\12" # => "\n"
"\1" # string with one character with code point 1
A backslash followed by a u denotes a unicode codepoint. It can either be followed by exactly four hexadecimal characters representing the unicode bytes (\u0000 to \uFFFF) or a number of one to six hexadecimal characters wrapped in curly braces (\u{0} to \u{10FFFF}.1
2
3"\u0041"
"\u{41}"
"\u{1F52E}"
One curly brace can contain multiple unicode characters each separated by a whitespace.1
"\u{48 45 4C 4C 4F}"
字符串在运行时允许嵌入表达式1
2
3a = 1
b = 2
"sum: #{a} + #{b} = #{a + b}" # => "sum: 1 + 2 = 3"
字符串中允许插入任何表达式,但为了保证程序可读性,表达式尽量简短。
在#前加入反斜杠可以禁止插值, 也可以使用表达式: %q()1
2"\#{a + b}" # => "#{a + b}"
%q(#{a + b}) # => "#{a + b}"
通过调用String::Builder以及在每个#{…}之后调用Object#to_s(IO)实现字符串插值。表达式”sum: #{a} + #{b} = #{a + b}”与下方的等价1
2
3
4
5
6
7
8String::Builder.new do |io|
io << "sum: "
io << a
io << " + "
io << b
io << " = "
io << a + b
end
除了双引号之外,字符串还支持百分号(%)+一双分隔符的形式。可以使用的分隔符有(),[],{},<>,以及|| 。这可以用来处理字符串中带有双引号的情况。1
2
3
4
5%(hello ("world"))
%[hello ["world"]]
%{hello {"world"}}
%<hello <"world">>
%|hello "world"|
%q表示不转义插值, %Q则等价于%1
2
3name = "world"
%q(hello \n #{name}) # => "hello \\n \#{name}"
%Q(hello \n #{name}) # => "hello \n world"
1 | "hello |
一个字符串可以被分割成多行, 只需在每行的末尾加上反斜杠1
2
3"hello " \
"world, " \
"no newlines" # same as "hello world, no newlines"
1 | "hello \ |
heredoc以 <<- 紧跟一个标识符开始, 以标识符开头的一个新行作为结束。1
2
3
4
5<<-XML
<parent>
<child />
</parent>
XML
heredoc中允许调用方法或使用函数,变量等1
2
3
4
5
6
7
8
9
10
11<<-SOME
hello
SOME.upcase # => "HELLO"
def upcase(string)
string.upcase
end
upcase(<<-SOME
hello
SOME) # => "HELLO"
如果要在heredoc中禁用转义和插值,可以在定义heredoc的标识符上加上单引号1
2
3<<-'HERE'
hello \n #{world}
HERE # => "hello \n #{world}"
符号是一个被命名的常量,并且不能用整数赋值 。在内部,一个符号被表示成一个 Int32。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:hello
:good_bye
# With spaces and symbols
:"symbol with spaces"
# Ending with question and exclamation marks
:question?
:exclamation!
# For the operators
:+
:-
:*
:/
:==
:<
:<=
:>
:>=
:!
:!=
:=~
:!~
:&
:|
:^
:~
:**
:>>
:<<
:%
:[]
:[]?
:[]=
:<=>
:===
数组是一个带数字索引的某个数据类型(type T)的值的一个集合。用方括号括起,用逗号分隔 。1
[1, 2, 3]
数组的类型可以是单一的 或多种类型的混合。1
2[1, 2, 3] # => Array(Int32)
[1, "hello", 'x'] # => Array(Int32 | String | Char)
规定单一类型的数组可以在数组定义时的末尾加上 “of type” ,空数组必须指明类型。1
2
3
4
5
6array_of_numbers = [1, 2, 3] of Number # => Array(Number)
array_of_numbers + [0.5] # => [1, 2, 3, 0.5]
array_of_int_or_string = [1, 3, 4] of Int32 | String # => Array(Int32 | String)
array_of_int_or_string + ["foo"] # => [1, 2, 3, "foo"]
[] of Int32 # => Array(Int32).new
可以用带百分号的方法来创建字符串数组和符号数组。1
2%w(one two three) # => ["one", "two", "three"]
%i(one two three) # => [:one, :two, :three]
使用大括号表示,元素之间使用逗号分隔。1
Array{1, 2, 3}
这种写法适用于任意类型的数组。
This literal can be used with any type as long as it has an argless constructor and responds to <<.1
2IO::Memory{1, 2, 3}
Set{1, 2, 3}
对于像IO::Memory这样不寻常的类型,等价于1
2
3
4array_like = IO::Memory.new
array_like << 1
array_like << 2
array_like << 3
对于像Set这样的普通类型, the generic type T is inferred from the types of the elements in the same way as with the array literal. The above is equivalent to:1
2
3
4array_like = Set(typeof(1, 2, 3)).new
array_like << 1
array_like << 2
array_like << 3
类型的参数可以作为类型名称的一部分显式的指定。1
Set(Number) {1, 2, 3}
1 | {1 => 2, 3 => 4} # Hash(Int32, Int32) |
哈希的键和值都可以包含多种数据类型。当创建一个空的哈希表时 ,必须分别指定键和值的数据类型。1
2{} of Int32 => Int32 # 等价于 Hash(Int32, Int32).new
{} # 此种玩法将报错
你可以使用哈希数据跟其它数据类型一起玩耍。as long as they define an argless new method and a []= method:1
MyType{"foo" => "bar"}
If MyType is not generic, 上面等价于1
2
3tmp = MyType.new
tmp["foo"] = "bar"
tmp
If MyType is generic,上面等价于1
2
3tmp = MyType(typeof("foo"), typeof("bar")).new
tmp["foo"] = "bar"
tmp
一般数据类型也可以这么搞1
MyType(String, String) {"foo" => "bar"}
1 | x..y # 包含末尾, in mathematics: [x, y] |
1 | foo_or_bar = /foo|bar/ |
正则使用斜杠分隔 ,并且使用 PCRE语法 。
可以跟这些参数1
2
3
4
i: ignore case (PCRE_CASELESS)
m: multiline (PCRE_MULTILINE)
x: extended (PCRE_EXTENDED)
例子1
2
3r = /foo/imx
slash = /\//
r = %r(regex with slash: /) # 这是另一种写法
创建和使用方法如下 , 创建空元组可以使用 Tuple.new1
2
3
4tuple = {1, "hello", 'x'} # Tuple(Int32, String, Char)
tuple[0] #=> 1 (Int32)
tuple[1] #=> "hello" (String)
tuple[2] #=> 'x' (Char)
要规定元组的数据类型,可以这么玩1
2
3
4
5# The type denoting a tuple of Int32, String and Char
Tuple(Int32, String, Char)
# An array of tuples of Int32, String and Char
Array({Int32, String, Char})
1 | tuple = {name: "Crystal", year: 2011} # NamedTuple(name: String, year: Int32) |
一个过程可以理解为一段特定功能的代码。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19# A proc without arguments
->{ 1 } # Proc(Int32)
# A proc with one argument
->(x : Int32) { x.to_s } # Proc(Int32, String)
# A proc with two arguments:
->(x : Int32, y : Int32) { x + y } # Proc(Int32, Int32, Int32)
Proc(Int32, String).new { |x| x.to_s } # Proc(Int32, String) #另一种写法
# 接收一个Int32类型的参数, 返回一个String类型
Proc(Int32, String)
# A proc accepting no arguments and returning Void
Proc(Void)
# A proc accepting two arguments (one Int32 and one String) and returning a Char
Proc(Int32, String, Char)
参数的类型必须指定 ,except when directly sending a proc literal to a lib fun in C bindings.
返回值的类型会在proc过程中自动确定。
1 | proc = ->(x : Int32, y : Int32) { x + y } |
1 | # 赋值给一个本地变量 |
1 | name, age = "Crystal", 1 |
1 | if some_condition |
大的程序一般都会按照一定的分割方式,将代码写到不同的文件中。使用时可以用require “…” 的方式来组合这些程序。
require接受一个字符串的参数 。
require “filename”
这种写法将会在 require 的路径中查找 filename
默认情况下 ,require路径是编译器指定的标准库路径,并且是相对于当前路径的一个叫做“lib”的路径。
查找的规则如下:
如果文件 “filename.cr”在 require路径中存在, 它将被召唤
如果存在一个”filename”的目录并且在此目录中存在 “filename.cr”,它将被召唤
否则 程序抛出异常1
require "foo/bar/baz"
require “./filename”
这种召唤使用的路径是相对于当前调用require的程序文件的。可以使用 ../ 或../../等进行上层目录的require1
require "./foo/bar/baz"
可以使用通配符进行require1
2require "foo/*" #将会require foo目录下的所有 .cr文件
require "foo/**" # 将会 require foo及其子目录下的所有 .cr文件
在Crystal中,一切都是对象。
1 | person = Person.new # 创建一个实例 |
Crystal的标准库定义了宏来使getter和setter变的简单。1
2
3
4
5
6
7
8
9
10
11
12class Person
property age
getter name : String
def initialize(@name)
@age = 0
end
end
john = Person.new "John"
john.age = 32
john.age #=> 32
类的方法允许分开写,在编译的时候将会被合并到一个类中1
2
3
4
5
6
7
8
9
10
11class Person
def initialize(@name : String)
@age = 0
end
end
class Person
def become_older
@age += 1
end
end
注:方法重定义时,后面定义的方法将会覆盖之前的方法。
你可以通过调用 previous_def 在调用之前定义的方法:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16class Person
def become_older
@age += 1
end
end
class Person
def become_older
previous_def # 不带括号和参数的情况下 ,将接收当前方法调用时的参数 否则使用自定义参数。
@age += 2
end
end
person = Person.new "John"
person.become_older
person.age #=> 3
对象的属性也可以在构造子之外被初始化1
2
3
4
5
6class Person
@age = 0
def initialize(@name : String)
end
end
This will initialize @age to zero in every constructor. This is useful to avoid duplication, but also to avoid the Nil type when reopening a class and adding instance variables to it.
看下面的例子1
2
3
4
5class Person
def initialize(@name)
@age = 0
end
end
@age毫无疑问是一个integer,但是@name的类型不好判断了。 编译器会结合所有用到Person类的地方对@name的类型进行判断,但是这么搞会有点问题:
对编译器不友好,也不利于增加代码的可读性 。随着项目代码量的增加,这个问题会变的严重。编译变慢,代码也看不懂了 。。。
Crystal有以下几种方法来杜绝这个问题
1.显式的注明类型1
2
3
4
5
6
7
8class Person
@name : String
@age : Int32
def initialize(@name)
@age = 0
end
end
2.定义时赋于具体的值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
50class Person
def initialize
@name = "John Doe" # 用值墳充
@age = 0 # 用值墳充
end
end
class Person
def initialize
@address = Address.new("somewhere") #对象调用赋值
end
end
class Something
def initialize
@values = Array(Int32).new #数组
end
end
class Person
def initialize(name : String) #用参数赋值
@name = name
end
end
class Person
def initialize(@name : String) #同上等价
end
end
class Person
def initialize(name = "John Doe")
@name = name
end
end
class Person
def initialize(@name = "John Doe")
end
end
class Person
def initialize
@age = LibPerson.compute_default_age # 使用库方法的返回值
end
end
lib LibPerson
fun compute_default_age : Int32
end
1 | class Person |
1 | class Person |
通过使用 * ,方法可以接收可变数量的参数。在方法内部 ,参数转化为元组。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24def sum(*elements)
total = 0
elements.each do |value|
total += value
end
total
end
sum 1, 2, 3 #=> 6
sum 1, 2, 3, 4.5 #=> 10.5
# elements is Tuple(Int32, Int32, Int32, Float64)
sum 1, 2, 3, 4.5
def sum(*elements, initial = 0) # 可变参数的后面还可以跟命名参数
total = initial
elements.each do |value|
total += value
end
total
end
sum 1, 2, 3 # => 6
sum 1, 2, 3, initial: 10 # => 16
双星号(**) 会匹配命名元组构成的参数中的 没有被匹配到的参数形成的元组1
2
3
4
5
6
7
8
9
10
11
12
13
14
15def foo(x, **other)
# Return the captured named arguments as a NamedTuple
other
end
foo 1, y: 2, z: 3 # => {y: 2, z: 3}
foo y: 2, x: 1, z: 3 # => {y: 2, z: 3}
# 将元组送入参数
def foo(x, y)
x - y
end
tuple = {y: 3, x: 10}
foo **tuple # => 7
1 | class Parent |
模块的定义有两个用途:
1.为定义新的数据类型,方法和常量提供命名空间
2.作为可以被其它代码块调用的代码片段 (as partial types that can be mixed in other types )
1 | module Curses |
自定义库文件时建议创建命名空间 以防止出现冲突。标准库没有使用命名空间,因为使用比较频繁 这样可以方便书写。
模块允许再调用别的模块。
模块不能被实例化。
作为代码片段时可以通过 include或extend来调用 。include 和extend 允许在代码的顶层调用,to avoid writing a namespace over and over
1 | module ItemsSize |
可以使用结构体替代Class来创建新的类型1
2
3
4
5
6struct Point
property x, y
def initialize(@x : Int32, @y : Int32)
end
end
结构体和类的区别
1.在结构体上调用new方法,将在栈上开辟空间而不是堆
2.结构体是传值调用而类是传址
3.结构体无法继承一个非抽象结构体
The last point has a reason to it: a struct has a very well defined memory layout. For example, the above Point struct occupies 8 bytes. If you have an array of points the points are embedded inside the array’s buffer:1
2# The array's buffer will have 8 bytes dedicated to each Point
ary = [] of Point
If Point is inherited, an array of such type must also account for the fact that other types can be inside it, so the size of each element must grow to accommodate that. That is certainly unexpected. So, non-abstract structs can’t be inherited. Abstract structs, on the other hand, will have descendants, so it’s expected that an array of them will account for the possibility of having multiple types inside it.
A struct can also include modules and can be generic, just like a class.
A struct is mostly used for performance reasons to avoid lots of small memory allocations when passing small copies might be more efficient.
So how do you choose between a struct and a class? The rule of thumb is that if no instance variable is ever reassigned, i.e. your type is immutable, you could use a struct, otherwise use a class.
常量可以在任意地方定义 , 首字母必须大写 ,通常整个写成大写。1
2
3
4
5
6
7
8PI = 3.14
module Earth
RADIUS = 6_371_000
end
PI #=> 3.14
Earth::RADIUS #=> 6_371_000
常量允许调用方法,并且可以有复杂的逻辑封装。1
2
3
4
5
6
7
8
9TEN = begin
a = 0
while a < 10
a += 1
end
a
end
TEN #=> 10
Crystal中内置了一些常量1
2
3
4
5
6
7__LINE__ 定义了程序中的当前行数,当它被声明为一个方法的参数时 它代表了在该方法中的行号。
__END_LINE__ is the line number of the end of the calling block. Can only be used as a default value to a method parameter.
__FILE__ 代表了当前被调用的程序文件的完整路径
__DIR__ 表示被调用程序文件所在的完整目录
方法定义时可以接受带有yield关键字的代码块。1
2
3
4
5
6
7
8
9def twice
yield
yield
end
# 这里将会输出Hello两次
twice do
puts "Hello!"
end
定义时只需要在方法中加入yield关键词,也可以通过 & 符号显式的定义。1
2
3
4def twice(&block)
yield
yield
end
要调用方法和传入块代码时,只需要使用 do … 或 {…}的形式。1
2
3
4
5
6
7
8
9twice() do
puts "Hello!"
end
twice do
puts "Hello!"
end
twice { puts "Hello!" }
两者的区别在于 do … end and { … } is that do … end binds to the left-most call, while { … } binds to the right-most call:1
2
3
4
5
6
7
8
9
10
11foo bar do
something
end
foo(bar) do
something
end
foo bar { something }
foo(bar { something })
1 | def twice |
一段代码可以被捕获并转换成Block
定义一个block1
2
3
4
5
6def int_to_int(&block : Int32 -> Int32)
block
end
proc = int_to_int { |x| x + 1 }
proc.call(1) #=> 2
下面的例子定义一个块,作为回调来使用1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24class Model
def on_save(&block)
@on_save_callback = block
end
def save
if callback = @on_save_callback
callback.call
end
end
end
model = Model.new
model.on_save { puts "Saved!" }
model.save # prints "Saved!"
# 上例没有指明 &block的类型, 用来表明 不接收任何参数 也不返回任何值
# 注:如果没有指明返回类型, 将不返回任何值
def some_proc(&block : Int32 ->)
block
end
proc = some_proc { |x| x + 1 }
proc.call(1) # void
break 和 next
return和break不能用在block中 ,使用next会退出block并返回值
with …yield
在block中无效
定义一个block和Proc,并将block放入proc中执行1
2
3
4
5
6
7
8
9
10def some_proc(&block : Int32 -> Int32)
block
end
x = 0
proc = ->(i : Int32) { x += i }
proc = some_proc(&proc)
proc.call(1) #=> 1
proc.call(10) #=> 11
x #=> 11
一个Proc也可以通过一个现成的方法来创建1
2
3
4
5
6def add(x, y)
x + y
end
adder = ->add(Int32, Int32)
adder.call(1, 2) #=> 3
1 | raise "OH NO!" |
1 | class MyException < Exception |
使用 begin … rescue … end 的代码结构来处理异常: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
60begin
raise "OH NO!"
rescue
puts "Rescued!"
end
# 可以在rescue后新增参数来接收详细的异常信息
begin
raise "OH NO!"
rescue ex
puts ex.message
end
# 处理具体的异常
begin
raise MyException.new("OH NO!")
rescue MyException
puts "Rescued MyException"
end
begin
raise MyException.new("OH NO!")
rescue ex : MyException
puts "Rescued MyException: #{ex.message}"
end
# 多重异常处理
begin
# ...
rescue ex1 : MyException
# only MyException...
rescue ex2 : MyOtherException
# only MyOtherException...
rescue
# any other kind of exception
end
begin
# ...
rescue ex : MyException | MyOtherException
# only MyException or MyOtherException
rescue
# any other kind of exception
end
# else
begin
something_dangerous
rescue
# execute this if an exception is raised
else
# execute this if an exception isn't raised
end
# ensure ,相当于finally , 不论有无异常 都会执行
begin
something_dangerous
ensure
puts "Cleanup..."
end
当需要
1.指定限制类型
2.指定参数类型
3.声明变量
4.声明别名
5.定义新类型
6.认证 is_a? 的call调用
7.认证as表达式
8.认证sizeof表达式
9.认证instance_sizeof表达式
10.方法的返回值类型
一些常见的类型都有简洁的操作方法。这对于当需要书写C库绑定时很有用,并且也可以用于上面的所有情况。
常规类型的声明写法1
2
3Int32
My::Nested::Type
Array(String)
多种类型联合1
alias Int32OrString = Int32 | String
空值1
2
3alias Int32OrNil = Int32?
alias Int32OrNil = Int32 | ::Nil
指针1
2
3alias Int32Ptr = Int32*
alias Int32Ptr = Pointer(Int32)
静态数组1
2
3alias Int32_8 = Int32[8]
alias Int32_8 = StaticArray(Int32, 8)
元组1
2
3alias Int32StringTuple = {Int32, String}
# 等价于
alias Int32StringTuple = Tuple(Int32, String)
命名元组1
2
3alias Int32StringNamedTuple = {x: Int32, y: String}
# 等价于
alias Int32StringNamedTuple = NamedTuple(x: Int32, y: String)
过程(Proc)1
2
3
4
5
6
7
8
9
10
11alias Int32ToString = Int32 -> String
# 等价于
alias Int32ToString = Proc(Int32, String)
# To specify a Proc without arguments:
alias ProcThatReturnsInt32 = -> Int32
# To specify multiple arguments:
alias Int32AndCharToString = Int32, Char -> String
# Proc嵌套需要使用括号
alias ComplexProc = (Int32 -> Int32) -> String
self
self can be used in the type grammar to denote a self type. Refer to the type restrictions section.
class1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24def foo(x : Int32)
"instance"
end
def foo(x : Int32.class)
"class"
end
foo 1 # "instance"
foo Int32 # "class"
# class也可以用来创建一个class类型的数组
class Parent
end
class Child1 < Parent
end
class Child2 < Parent
end
ary = [] of Parent.class
ary << Child1
ary << Child2
下划线(Underscore)
下划线可以用来表示任何数据类型1
2
3
4
5
6
7# Same as not specifying a restriction, not very useful
def foo(x : _)
end
# A bit more useful: any two arguments Proc that returns an Int32:
def foo(x : _, _ -> Int32)
end
typeof
用于返回传入参数的类型(相同的类型不会重复)1
2typeof(1 + 2) # => Int32
typeof(1, "a") # => (Int32 | String)
用于判断变量在运行时的类型是否和表达式匹配1
2
3
4
5a = 1
a.is_a?(Int32) #=> true
a.is_a?(String) #=> false
a.is_a?(Number) #=> true
a.is_a?(Int32 | String) #=> true
判断表达式的值是否为nil1
2
3
4
5a = 1
a.nil? # => false
b = nil
b.nil? # => true
判断一个变量是否有参数指定的方法1
2
3a = 1
a.responds_to?(:abs) #=> true
a.responds_to?(:size) #=> false
as 用于约束一个表达式的值类型。参数为一个数据类型。
注意: as不能用于做数据类型转换。Methods on integers, floats and chars are provided for these conversions.1
2
3
4
5
6
7if some_condition
a = 1
else
a = "hello"
end
# a : Int32 | String
在上面 ,a的类型为 Int32 | String。 在if段中可以强制编译器指定一个类型(Int)。as采用运行时检查,如果a不是Int32,将抛出异常。1
2a_as_int = a.as(Int32)
a_as_int.abs # works, compiler knows that a_as_int is Int32
Converting between pointer types
The as pseudo-method also allows to cast between pointer types:1
2ptr = Pointer(Int32).malloc(1)
ptr.as(Int8*) #:: Pointer(Int8)
同as一样, 但是在出现异常时只是返回nil ,并且也不支持指针类型同其它数据类型之间的转换。1
2
3
4value = rand < 0.5 ? -3 : nil
result = value.as?(Int32) || 10
value.as?(Int32).try &.abs
返回一个表达式值的类型
宏是一些预编译的方法(在编译时被加入到抽像语法树中),这些方法必须是完整有效的Crystal代码。1
2
3
4
5
6
7
8
9
10
11
12
13
14macro define_method(name, content)
def {{name}}
{{content}}
end
end
# This generates:
#
# def foo
# 1
# end
define_method foo, 1
foo #=> 1
宏被定义在顶层作用域,所有代码中都可用。如果某个宏被标记为private 那么它只在当前文件中可见。
宏也可以定义在类和模块中, 并且在定义它的作用域中可见。程序也会在先祖域中搜索宏(父类 及被包含的模块中)。
For example, a block which is given an object to use as the default receiver by being invoked with with … yield can access macros defined within that object’s ancestors chain:1
2
3
4
5
6
7
8
9
10
11class Foo
macro emphasize(value)
"***#{ {{value}} }***"
end
def yield_with_self
with self yield
end
end
Foo.new.yield_with_self { emphasize(10) } #=> "***10***"
定义在类中或模块中的宏 也可以在外部被调用。1
2
3
4
5
6
7class Foo
macro emphasize(value)
"***#{ {{value}} }***"
end
end
Foo.emphasize(10) # => "***10***"
1 | 使用 { } 在语法树中进行插入值。 |
Note that :foo was the result of the interpolation, because that’s what was passed to the macro. You can use the method ASTNode#id in these cases, where you just need an identifier.
你可以在编译时调用一些方法, 这些方法被定义在Crystal::Macros 模块中。
1 | # 你可以使用{% if condition %} ... {% end %} 来按条件生成代码。宏的条件语句可以在宏定义之外的地方使用。 |
迭代语句也可以在宏定义之外的地方使用。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
36macro define_dummy_methods(names)
{% for name, index in names %}
def {{name.id}}
{{index}}
end
{% end %}
end
define_dummy_methods [foo, bar, baz]
foo #=> 0
bar #=> 1
baz #=> 2
#
macro define_dummy_methods(hash)
{% for key, value in hash %}
def {{key.id}}
{{value}}
end
{% end %}
end
define_dummy_methods({foo: 10, bar: 20})
foo #=> 10
bar #=> 20
#
{% for name, index in ["foo", "bar", "baz"] %}
def {{name.id}}
{{index}}
end
{% end %}
foo #=> 0
bar #=> 1
baz #=> 2
在一些场景下,一些特殊的宏被当成钩子来使用。这些场景是 inherited, included, extended 和 method_missing1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
181.inherited在编译时被触发,如果定义了子类。@type is the inheriting type.
2.included在编译时被触发,当一个模块被include时。@type is the including type.
3.extended在编译时被触发,当一个模块被extend时。@type is the extending type.
4.method_missing 在编译时被触发,当方法不存在时.
例子: inherited
class Parent
macro inherited
def lineage
"{{@type.name.id}} < Parent"
end
end
end
class Child < Parent
end
Child.new.lineage #=> "Child < Parent"
例子: method_missing1
2
3
4
5
6macro method_missing(call)
print "Got ", {{call.name.id.stringify}}, " with ", {{call.args.size}}, " arguments", '\n'
end
foo # Prints: Got foo with 0 arguments
bar 'a', 'b' # Prints: Got bar with 2 arguments
以下是标准库的代码风格,遵守这种写法可以使你的代码风格和其它开发者统一。
类型名使用驼峰方式1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class ParseError < Exception
end
module HTTP
class RequestHandler
end
end
alias NumericValue = Float32 | Float64 | Int32 | Int64
lib LibYAML
end
struct TagDirective
end
enum Time::DayOfWeek
end
方法名采用下划线分割1
2
3
4
5
6
7
8
9
10class Person
def first_name
end
def date_of_birth
end
def homepage_url
end
end
变量名使用下划线分割1
2
3
4
5
6
7
8
9
10
11class Greeting
@@default_greeting = "Hello world"
def initialize(@custom_greeting = nil)
end
def print_greeting
greeting = @custom_greeting || @@default_greeting
puts greeting
end
end
常量使用下划线分割1
2LUCKY_NUMBERS = [3, 7, 11]
DOCUMENTATION_URL = "http://crystal-lang.org/docs"
首字母
类名中的首字母全部使用大写 , 方法名中全部是小写 。
库名
库名全部以Lib开头 ,如LibC ,LibEvent2
目录结构和文件
在一个project中 ,
1./ 下包含readme,项目配置 以及项目文档
2.src/ 下包含源码
3.spec/ 下包含项目的 spec , 可以通过 crystal spec来运行
4.bin/ 下包含所有可执行文件
文件路径需要匹配命名空间 ,文件名以小写+”_”构成 ,如下1
HTTP::WebSocket 是定义在 src web_socket.cr
空格
使用两个空格做代码缩进
需要使用库来访问关系型数据库。crystal-lang/crystal-db 包提供了访问不同数据库的统一接口。
crystal-db支持下面的数据库类型:1
2
3crystal-lang/crystal-sqlite3 for sqlite
crystal-lang/crystal-mysql for mysql & mariadb
will/crystal-pg for postgres
除了标准统一的数据库接口外 ,不同的数据库驱动也会提供一些额外的接口。
需要配置项目中的shard.yml, 调用时不需要使用 “require crystal-lang/crystal-db” 下面是安装mysql的例子1
2
3dependencies:
mysql:
github: crystal-lang/crystal-mysql
使用DB.open和连接配置字符串可以很容易创建连接。下面是打开Mysql的例子1
2
3
4
5
6
7
8
9
10
11require "db"
require "mysql"
DB.open "mysql://root@localhost/test" do |db|
# ... use db to perform queries
end
# 其它的一些数据库连接字符串
sqlite3:///path/to/data.db
mysql://user:password@server:port/database
postgres://server:port/database
你也可以不通过yield来创建连接,这需要在程序结束前手动关系连接。1
2
3
4
5
6
7
8
9require "db"
require "mysql"
db = DB.open "mysql://root@localhost/test"
begin
# ... use db to perform queries
ensure
db.close
end
使用exec执行sql语句1
db.exec "create table contacts (name varchar(30), age int)"
使用带参数的形式来防止Sql注入1
2db.exec "insert into contacts values (?, ?)", "John", 30
db.exec "insert into contacts values (?, ?)", "Sarah", 33
注意:使用Pg数据库时要使用 $1,$2来替代 ?
使用query
通过使用query方法, 可以取到Sql的执行结果 ,参数的使用方法和exec一样。
query返回的rs需要关闭 , 如果是使用块方式进行的调用 rs将会被隐式的关闭。1
2
3
4
5db.query "select name, age from contacts order by age desc" do |rs|
rs.each do
# ... perform for each row in the ResultSet
end
end
crystal在编译时无法预知数据库返回记录的字段类型 ,你需要使用 rs.read(T) 使用T来指定期望获取的数据类型。1
2
3
4
5
6
7
8
9db.query "select name, age from contacts order by age desc" do |rs|
rs.each do
name = rs.read(String)
age = rs.read(Int32)
puts "#{name} (#{age})"
# => Sarah (33)
# => John Doe (30)
end
end
可以一次读取多个字段1
name, age = rs.read(String, Int32)
或者只读取一行1
name, age = db.query_one "select name, age from contacts order by age desc limit 1", as: { String, Int32 }
或者只读取某个值1
max_age = db.scalar "select max(age) from contacts"
所有这些操作方法都定义在 DB::QueryMethods
连接通常是指一个打开的Tcp连接或端口。socket会在某个时间处理一个事件。如果一个数据库程序在一个时间内需要处理很多查询或请求,相应的它就需要多个连接。
数据库服务和程序是相对独立的,我们可以使程序不需要关注数据库连接的断开,服务重启之类的事情 。为了达到这个小目标,数据库连接池通常是一个好的方案。
当数据库使用 crystal-db打开之后 , 连接池就建立了。db.open返回一个管理数据库连接池的DB::Database对象。1
2
3DB.open("mysql://root@localhost/test") do |db|
# db is a DB::Database
end
当执行 db.query, db.exec, db.scalar等时,连接池执行如下过程:1
2
3
4
51.在池中找到一个有效连接(在必要时会创建一个,如果无法创建 则等待一个有效连接 - 等待时间一般不能是很长)
2.提取出连接
3.执行Sql
4.如果不去取 DB::ResultSet, 连接会被放回连接池中。否则会等到ResultSet关闭时才把连接放回池中。
5.返回结果
1 | Name Default value |
当调用DB::Database时, initial_pool_size数量的连接将被创建。如果空闲的连接数量大于 max_idle_pool_size时, 返回池中的连接将被关闭。
当连接数达到max_pool_size时, 如果需要调用连接 则要等待checkout_timeout秒 直到现有的连接可用。
当一个连接丢失或无法使用时,将会尝试最多retry_attempts次,
简单的讲shards就是一些crystal的代码包,这些包可以被不同的项目共享使用。
举个例子,我们将创建一个叫做 palindrome-example的包
在这个例子中,需要准备如下环境配置:
1.带有Crystal编译器的环境
2.git
3.github帐号
通过命令行 crystal init lib 1
2
3
4
5
6
7
8
9
10
11
12$ crystal init lib palindrome-example
create palindrome-example/.gitignore
create palindrome-example/.editorconfig
create palindrome-example/LICENSE
create palindrome-example/README.md
create palindrome-example/.travis.yml
create palindrome-example/shard.yml
create palindrome-example/src/palindrome-example.cr
create palindrome-example/src/palindrome-example/version.cr
create palindrome-example/spec/spec_helper.cr
create palindrome-example/spec/palindrome-example_spec.cr
Initialized empty Git repository in /<YOUR-DIRECTORY>/.../palindrome-example/.git/
cd 到创建的Lib目录1
cd palindrome-example
通过git add和commit将文件提交到版本库1
2
3
4
5
6
7
8
9
10
11
12
13
14 $ git add -A
$ git commit -am "First Commit"
[master (root-commit) 77bad84] First Commit
10 files changed, 102 insertions(+)
create mode 100644 .editorconfig
create mode 100644 .gitignore
create mode 100644 .travis.yml
create mode 100644 LICENSE
create mode 100644 README.md
create mode 100644 shard.yml
create mode 100644 spec/palindrome-example_spec.cr
create mode 100644 spec/spec_helper.cr
create mode 100644 src/palindrome-example.cr
create mode 100644 src/palindrome-example/version.cr
书写和测试代码 ,Crystal有一个内建的测试库可以用来测试。
书写代码文档,Crystal有一个内建的文档库可以使用。
可以运行 crystal docs命令来创建API文档,使用浏览器打开/docs/目录可以看到文档。
下面讲解将编译器生成的页面发布到github
文档准备好之后需要添加README.md文件(记得要替换掉 1
[![Docs](https://img.shields.io/badge/docs-available-brightgreen.svg)](<LINK-TO-YOUR-DOCUMENTATION>)
书写README.md, 这个文件必须要包含以下几点:
1.介绍一下你的包
2.说明它的作用
3.如何使用它
文中最好包含少量的例子说明。
注意:确保把文中所有的 [your-github-name] 都替换成你自己的github用户名
编码风格
与标准库的写法保持一致是最好的1
2crystal tool format # 可以用于检查你的代码格式是否正确
crystal tool format --check # 只做检查,不做任何的代码改动
创建 shards.yml1
name: palindrome-example
description: 是一个单行文本, 用来描述你的库 以及用于被搜索
在github上创建一个与name,和描述都同名的仓库 ,然后提交1
2
3
4$ git add -A && git commit -am "shard complete"
$ git remote add public https://github.com/<YOUR-GITHUB-NAME>/<YOUR-REPOSITORY-NAME>.git
$ git push public master # 推送到github