类型和方法

类型和方法

类和方法

Crystal中一切都是对象

person = Person.new  # 创建一个对象

## 定义一个类
class Person
  def initialize(name : String)  # 构造子 ,没有赋于具体值的属性需要指定类型
    @name = name
    @age = 0
  end
  # 也可以这样写
  def initialize(@name : String) # 直接在此处定义了一个属性
    @age = 0
  end

  def name
    @name
  end

  def age
    @age
  end
end

## 调用
john = Person.new "John"
peter = Person.new "Peter"

john.name #=> "John"
john.age #=> 0

peter.name #=> "Peter"

## getter 和 setter方法 。Crystal在标准库中已经定义了相关的宏简化操作 ,参看
[getter](https://crystal-lang.org/api/0.24.1/Object.html#getter%28%2Anames%29-macro)
[settter](https://crystal-lang.org/api/0.24.1/Object.html#setter%28%2Anames%29-macro)
[property](https://crystal-lang.org/api/0.24.1/Object.html#property%28%2Anames%29-macro)

## 方法重载,previous_def
class Person
  def become_older
    @age += 1
  end
end

class Person
  def become_older
    @age += 2
  end
end

person = Person.new "John"
person.become_older
person.age #=> 2

## 重载方法中调用之前的定义
class 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

类型推断

crystal的设计哲学是尽可能的不要求显式指定数据类型,但也有一些情况下是必须指定类型的。

class Person
  def initialize(@name)
    @age = 0
  end
end

在上面代码中@name没有指明类型,crystal编译器会扫描所有Person被调用的地方,这样会有以下问题:

  • 代码可读性差
  • 不利于编译优化,当代码变复杂时编译时间会变的很长
    因此,需要指明类和实例的属性变量类型。

直接指定数据类型


class Person
  @name : String
  @age : Int32

  def initialize(@name)
    @age = 0
  end
end

不指定类型

编译器会使用一系列的规则去匹配,当没有规则匹配成功时变量的类型将会是一个包含各种类型的联合类型。在变量没有初始化的情况下,联合中还将包括Nil类型。规则有很多,但常用的一般是前3个匹配,无需了解。当编译器报错时,显式的指定类型即可。下面的规则对于类和实例都适用:

  • 向变量赋于一个字面量值, 下面的例子@name将被推断为String类型,@age将是Int32类型。
class Person
  def initialize
    @name = "John Doe"
    @age = 0
  end
end
  • 使用类的new方法进行变量赋值,下面的例子@address的类型将被推断为Address
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  # @name将被推断为String类型
  end
end

#精简语法
class Person
  def initialize(@name : String)
  end
end
``` ```
## 这个例子,编译会认为@name是String类型, 在编译时将报错
class Person
  def initialize(name : String)
    name = 1
    @name = name
  end
end
  • 使用一个有返回值类型的类方法赋值
class Person
  def initialize
    @address = Address.unknown
  end
end

class Address
  # No need for a return type annotation here
  def self.unknown
    new("unknown")
  end

  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
  • 使用Lib库的调用结果赋值
class Person
  def initialize
    @age = LibPerson.compute_default_age
  end
end

lib LibPerson
  fun compute_default_age : Int32
end
  • 使用out lib expression因为lib function必定有指定类型,编译器可以使用out指定参数类型(通常是指针)
## @age类型是Int32
class Person
  def initialize
    LibPerson.compute_default_age(out @age)
  end
end

lib LibPerson
  fun compute_default_age(age_ptr : Int32*)
end
  • 其它规则 if: 将会对所有的流程分支进行扫描,推断的类型一般是所有分支产生类型的一个联合。@abc ||= 55: @abc类型将被推断为 Int32 | Nil常量赋值推断:
## @luck_number类型为Int32
class SomeObject
  DEFAULT_LUCKY_NUMBER = 42

  def initialize(@lucky_number = DEFAULT_LUCKY_NUMBER)
  end
end

联合类型(Union types)

表达式或变量可以由多种类型组成,称之为联合。

if 1 + 2 == 3
  a = 1
else
  a = "hello"
end

a # : Int32 | String

上面的代码片段,a将获取 Int32和String两种类型,这个是在编译时由编译器确定。在运行时,a只可能取得一种类型。

## The runtime type
a.class # => Int32

## The compile-time type
typeof(a) # => Int32 | String

如果有需要,变量可以在编译时被定义为联合类型

## set the compile-time type
a = 0.as(Int32|Nil|String)
typeof(a) # => Int32 | Nil | String

联合的类型规则 :一般情况下相T1和T2两种类型联合,联合的类型将会是T1 | T2 , 但也有例外。1. 继承相同Class的类的联合,返回类型为 class+

class Foo
end

class Bar < Foo
end

class Baz < Foo
end

bar = Bar.new
baz = Baz.new

## Here foo's type will be Bar | Baz,
## but because both Bar and Baz inherit from Foo,
## the resulting type is Foo+
foo = rand < 0.5 ? bar : baz
typeof(foo) # => Foo+

  • 相同大小的元组形成的联合,返回类型为所有元组数据类型的集合。
t1 = {1, "hi"}   # Tuple(Int32, String)
t2 = {true, nil} # Tuple(Bool, Nil)

t3 = rand < 0.5 ? t1 : t2
typeof(t3) # Tuple(Int32 | Bool, String | Nil)
  • 带有相同Key的命名元祖构成的集合, 看例子
t1 = {x: 1, y: "hi"}   # Tuple(x: Int32, y: String)
t2 = {y: true, x: nil} # Tuple(y: Bool, x: Nil)

t3 = rand < 0.5 ? t1 : t2
typeof(t3) # NamedTuple(x: Int32 | Nil, y: String | Bool)

重载

类和类的方法都可以重载,方法重载可以写在同一个类定义中 也可以重复定义相同的类。最新的定义将覆盖之前的定义。

默认值和命名参数

class Person
  def become_older(by = 1)
    @age += by
  end
end

def some_method(x, y = 1, z = 2, w = 3)
  # do something...
end

some_method 10                   # x: 10, y: 1, z: 2, w: 3
some_method 10, z: 10            # x: 10, y: 1, z: 10, w: 3
some_method 10, w: 1, y: 2, z: 3 # x: 10, y: 2, z: 3, w: 1
some_method y: 10, x: 20         # x: 20, y: 10, z: 2, w: 3

some_method y: 10                # Error, missing argument: x

*与元组

通过使用*,方法可以接收不固定数量的参数。

def 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)
sum 1, 2, 3
## 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

## 方法多态
def foo(*elements, x)
  1
end

def foo(*elements, y)
  2
end

foo x: "something" # => 1
foo y: "something" # => 2

## *参数 也可以只用一个*表示 这时它并不是一个参数,它的意思是: 在方法调用时 *位之后的参数必须是带名称的
def foo(x, y, *, z)
end

foo 1, 2, 3    # Error, wrong number of arguments (given 3, expected 2)
foo 1, 2       # Error, missing argument: z
foo 1, 2, z: 3 # OK

## 在元组之前加* , 可以把元组展开 传入方法中
def foo(x, y)
  x + y
end

tuple = {1, 2}
foo *tuple # => 3

## 双*(**) ,可以展开一个命名的元组 传入方法中
def foo(x, y)
  x - y
end

tuple = {y: 3, x: 10}
foo **tuple # => 7

## 在定义时指定**
def 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 add(x : Number, y : Number)
  x + y
end

## Ok
add 1, 2 # Ok

## Error: no overload matches 'add' with types Bool, Bool
add true, false

## self restriction
...略

## 字面量的类型限制
定义一个方法只接收参数Int32(不是实例), 可以使用 .class后缀
def foo(x : Int32.class)
end

foo Int32  # OK
foo String # Error

## 可变参数中的类型限制
def foo(*args : Int32)
end

def foo(*args : String)
end

foo 1, 2, 3       # OK, invokes first overload
foo "a", "b", "c" # OK, invokes second overload
foo 1, 2, "hello" # Error
foo()             # Error , 空参数不能与上面的任意一个匹配,必须重写一个不接收参数的foo方法。

## 使用Object作为类型约束,是用来配对任意类型参数的简单方法。
def foo(*args : Object)
end

foo() # Error
foo(1) # OK
foo(1, "x") # OK

## 自由类型变量
通过使用forall , 可以使用一个或多个参数的类型来做类型限制
def foo(x : T) forall T
  T
end

foo(1)       #=> Int32
foo("hello") #=> String

返回类型

方法的返回类型是编译器自动确定的,但是也可以手动指定类型

def some_method : String
  "hello"
end

## 返回nil
指定Nil类型返回,程序就不需要在末尾再特别返回nil。使用Void也可以达到相同效果,在C绑定时,比较偏向使用Void
def some_method : Nil
  1 + 2
end

some_method # => nil

方法参数

…略

操作符

可以给类型赋于操作

struct Vector2
  getter x, y

  def initialize(@x : Int32, @y : Int32)
  end

  def +(other)
    Vector2.new(x + other.x, y + other.y)
  end
end

v1 = Vector2.new(1, 2)
v2 = Vector2.new(3, 4)
v1 + v2               #=> Vector2(@x=4, @y=6)

二进制操作符

+ – addition
- – subtraction
* – multiplication
/ – division
% – modulo
& – bitwise and
| – bitwise or
^ – bitwise xor
** – exponentiation
<< – shift left, append
>> – shift right
== – equals
!= – not equals
< – less
<= – less or equal
> – greater
>= – greater or equal
<=> – comparison
=== – case equality

下标操作符

[]  # array index (越界将报错)
[]? # array index (越界返回nil)
[]= # array index 赋值

## 例子
class MyArray
  def [](index)
    # ...
  end

  def [](index1, index2, index3)
    # ...
  end

  def []=(index, value)
    # ...
  end
end

array = MyArray.new

array[1]       # invokes the first method
array[1, 2, 3] # invokes the second method
array[1] = 2   # invokes the third method

array.[](1)       # invokes the first method
array.[](1, 2, 3) # invokes the second method
array.[]=(1, 2)   # invokes the third method

可见性

使用private关键词定义: private def xxx

1. 私有方法: 只能在类的内部以及子类中调用 并且不需要加 self
2. 私有类型:
class Foo
  private class Bar
  end

  Bar      # OK
  Foo::Bar # Error
end

Foo::Bar # Error

受保护的(protected)方法: protected def xxx只能被与当前类型同类型的,或与当前类型在相同命名空间中的(class, struct, module, etc.)实例调用。例子, …略

在最外层定义的私有方法 和私有类,只能在定义的当前文件中可见。

继承

class Person
  def initialize(@name : String)
  end

  def greet
    puts "Hi, I'm #{@name}"
  end
end

class Employee < Person
end

employee = Employee.new "John"
employee.greet # "Hi, I'm John"

如果子类中定义了initialize方法,父类的构造子将不被调用。

class Person
  def initialize(@name : String)
  end
end

class Employee < Person
  def initialize(@name : String, @company_name : String)
  end
end

Employee.new "John", "Acme" # OK
Employee.new "Peter" # Error: wrong number of arguments
                     # for 'Employee:Class#new' (1 for 2)

子类可以重载父类的方法(重写或者使用类型限制实现多态)。可以使用super关键词来调用父类方法

class Person
  def greet(msg)
    puts "Hello, #{msg}"
  end
end

class Employee < Person
  def greet(msg)
    super # Same as: super(msg)
    super("another message")  # 可以另外指定参数
  end
end

抽象类

…略

类方法

不需要通过实例化就可以调用

module CaesarCipher
  def self.encrypt(string : String)
    string.chars.map{ |char| ((char.upcase.ord - 52) % 26 + 65).chr }.join
  end
end

CaesarCipher.encrypt("HELLO") # => "URYYB"

def CaesarCipher.decrypt(string : String)
  encrypt(string)
end

类属性

在变量名称前使用 @@ , 存在于静态类中 所有实例可以共享之

class Counter
  @@instances = 0

  def initialize
    @@instances += 1
  end

  def self.instances
    @@instances
  end
end

Counter.instances #=> 0
Counter.new
Counter.new
Counter.new
Counter.instances #=> 3

解构方法

在一个实例被回收时调用 finalize

class Foo
  def finalize
    # Invoked when Foo is garbage-collected
    # Use to release non-managed resources (ie. C libraries, structs)
  end
end