以下各节从几方面介绍了符合语言习惯的 Julia 编码风格。这些规则都不是绝对的;它们仅仅是帮您熟悉这门语言,或是帮您可以在许多可替代性设计中能够做出选择的一些建议而已。
编写代码作为在一系列步骤中最高级的办法,是可以快速开始解决问题的,但您应该试着尽快把一个程序分成许多函数。函数具有更好的可重用性和可测试性,并可以更好阐明它们正在做什么,它们的输入和输出是什么。此外,由于 Julia 的编译器工作原理,在函数中的代码往往比最高级别的代码运行得更快。
同样值得强调的是,函数应该以参数来代替,而不是直接在全局变量(除了像 pi 那样的常量)上操作。
代码应尽可能通用。相较于这样的代码书写:
convert(Complex{Float64}, x)
使用有效的泛型函数是更好的:
complex(float(x))
第二种写法把 x
转换成一个适当的类型,而不是一直用一个相同的类型。
这种类型特点是特别地与函数自变量相关。例如,不声明一个参数是 Int
类型或 Int32
类型,如果在这种情况下还可以保持是任何整数,那就应该是用 Integer
抽象表达出来的。事实上,在许多情况下您都可以把自变量类型给忽视掉,除非一些需要消除歧义的时候,由于如果一个类型不支持任何必要操作就会被忽略,那么一个 MethodError
不管怎样也都会被忽略掉。(这被大家认为是 duck typing。)
例如,考虑以下 addone
函数中的定义,这个功能可以返回 1 加上它的自变量。
addone(x::Int) = x + 1 # works only for Int
addone(x::Integer) = x + one(x) # any integer type
addone(x::Number) = x + one(x) # any numeric type
addone(x) = x + one(x) # any type supporting + and one
最后一个 addone
的定义解决了所有类型的有关自变量的 one
函数(像 x
类型一样返回 1 值,可以避免不想要的类型提供)和 +
函数的问题。关键是要意识到,仅仅是定义通用的 addone(x) = x + one(x)
写法也是没有性能缺失的,因为 Julia 会根据需要自主编译到专业的版本。举个例子,您第一次调用 addone(12)
的时候, Julia 会自动为 x::Int
自变量编译一个 addone
函数,通过调用一个内联值 1
代替 one
。因此,上表前三个定义全都是重复的。
取代这种写法:
function foo(x, y)
x = int(x); y = int(y)
...
end
foo(x, y)
利用以下的写法更好:
function foo(x::Int, y::Int)
...
end
foo(int(x), int(y))
第二种写法更好的方式,因为 foo
并没有真正接受所有类型的数据;它真正需要的是 Int
S。
这里的一个问题是,如果一个函数本质上需要整数,可能更好的方式是强制调用程序来决定怎样转换非整数(例如最低值或最高值)。另一个问题是,声明更具体的类型会为未来的方法定义提供更多的“空间”。
取代这种写法:
function double{T<:Number}(a::AbstractArray{T})
for i = 1:endof(a); a[i] *= 2; end
a
end
利用以下写法更好:
function double!{T<:Number}(a::AbstractArray{T})
for i = 1:endof(a); a[i] *= 2; end
a
end
Julia 标准库在整个过程中使用以上约定,并且 Julia 标准库还包含一些函数复制和修饰形式的例子(例如 sort
和 sort!
),或是其它只是在修饰(例如 push!
, pop!
,splice!
)的例子。这对一些也要为了方便而返回修改后数组的函数来说是很典型的。
像 Union(Function,String)
这样的类型,说明你的设计有问题。
当使用 x::Union(Nothing,T)
时,想想把 x
转换成 nothing
这个选项是否是必要的。以下是一些可供选择的替代选项
x
一起初始化x
的类型x
的域,就把它们存储在字典中x
是 noting
时是否有一个简单的规则。例如,域通常是以 nothing
开始的,但是是在一些定义良好的点被初始化。在这种情况下,要首先考虑它可能没被定义。通常情况下,像下面这样创建数组是没什么帮助的:
a = Array(Union(Int,String,Tuple,Array), n)
在这种情况下 cell(n)
这样写更好一些。 这也有助于对编译器进行注释这一特定用途,而不是试图将许多选择打包成一种类型。
base/
相同的命名传统module SparseMatrix
,
immutable UnitRange
.maximum
, convert
). 在容易读懂的情况下把几
个单词连在一起写 (isequal
, haskey
). 在必要的情况下, 使用下划
线作为单词的分隔符. 下划线也可以用来表示多个概念的组合
(remotecall_fetch
相比 remotecall(fetch(...))
是一种更有效的
实现), 或者是为了区分 (sum_kbn
). 简洁是提倡的, 但是要避免缩写
(indexin
而不是 indxin
) 因为很难记住某些单词是否缩写或者怎么
缩写的.如果一个函数需要多个单词来描述, 想一下这个函数是否包含了多个概念, 这样 的情况下最好分拆成多个部分.
避免错误要比依赖找错好多了。
Julia 在 if 和 while 语句中不需要括号。所以要这样写:
if a == b
来取代:
if (a == b)
剪接功能参数可以让人很依赖。取代 [a..., b...]
这种写法,简单的 [a, b]
这样写就已经连接数组了。collect(a)
的写法要比 [a...]
好,但是因为 a
已经是可迭代的了,直接用 a
而不要把它转换到数组中也许会更好。
信号函数:
foo{T<:Real}(x::T) = ...
应该这样写:
foo(x::Real) = ...
特别是如果 T 没被用在函数主体。即使 T 被用在函数主体了,如果方便的话也可以被 typeof(x) 替代。这在表现上并没有什么差异。要注意的是,这不是对一般的静态参数都要谨慎,只是在它们不会被用到时要特别留心。
还要注意容器类型,特别是函数调用中可能需要的类型参数。可以到 FAQ 如何声明“抽象容器类型”的域 来查看更多信息。
一些如以下的定义是十分让人困扰的:
foo(::Type{MyType}) = ...
foo(::MyType) = foo(MyType)
您要决定问题的概念是应被写作 MyType
或是 MyType()
,并要坚持下去。
最好的类型是用默认的实例,并且在解决某些问题需要方法时,再添加包括 Type{MyType}
的一些方法好一些。
如果一个类型是一个有效的枚举,它就应该被定义为一个单一的(理想情况下不变的)类型,而枚举变量是它的实例。构造函数和一些转换可以检测值是否有效。这项设计最好把枚举做成抽象类型,把“值”做成其子类型。
您要注意什么时候一个 macros 可以真的代替函数。
在 macros 中调用 eval
实在是个危险的标志;这意味着 macros 只有在被最高级调用的时候才会工作。如果这样一个 macros 被写为一个函数,它将自然地访问它需要的运行时值。
如果您有一个使用本地指针的类型:
type NativeType
p::Ptr{Uint8}
...
end
不要像下面这样写定义:
getindex(x::NativeType, i) = unsafe_load(x.p, i)
问题是,这种类型的用户可能在不知道该操作是不安全的情况下就写 [i]
,这容易导致内存错误。
这样的函数应该能检查操作,以确保它是安全的,或是在它的名字中有不安全的地方时可以提醒调用程序。
像下面这样书写定义是有可能的:
show(io::IO, v::Vector{MyType}) = ...
这样写将提供一个特定新元素类型的向量的自定义显示。虽然很让人想尝试,但却是应该避免的。麻烦的是,用户会想用一个众所周知的类型比如向量在一个特定的方式下的行为,也会过度定制它的行为,这都会使工作更困难。
您一般要使用 isa
和 <:
(issubtype
) 来测试类型而不会用 ==
。在与已知的具体类型的类型进行比较时,要精确检查类型的的相等性(例如 T == Float64
),或者是您真的明白您究竟在干什么。
x->f(x)
高阶函数经常被用作匿名函数来调用,虽然这样很方便,但是尽量少这么写。例如,尽量把 map(x->f(x), a)
写成 map(f, a)
。