风格指南
编写函数,而不仅仅是脚本。
在顶层的一系列步骤中编写代码是开始解决问题的快速方法,但您应该尝试尽可能快地将程序划分为函数。 函数更好地可重用和可测试,以及明确执行哪些操作以及它们的输入和输出是什么。 此外,由于Julia编译器的工作方式,函数内部的代码往往比顶级代码运行得更快。
同样值得强调的是,函数应该接受参数,而不是直接与全局变量一起工作(类型常量除外 'pi')。
避免写太具体的类型。
代码应该尽可能通用。 而不是写作:
Complex{Float64}(x)
最好使用可用的通用功能。
complex(float(x))
第二个版本将`x’转换为适当的类型,而不是总是相同的类型。
此样式点与函数参数特别相关。 例如,不要将参数声明为具有类型’Int’或 'Int32',如果它真的可以是任何用抽象类型表示的整数 '整数'。 事实上,在许多情况下,您可能根本不指定参数的类型,除非需要将其与方法的其他定义明确区分开来,因为无论如何都会发生错误。 'MethodError',如果传递的类型不支持任何必需的操作。 (这就是所谓的https://en.wikipedia.org/wiki/Duck_typing [隐式键入]。)
例如,考虑`addone`函数的以下定义,它返回一个单元及其参数。
addone(x::Int) = x + 1 # Работает только для целочисленного типа
addone(x::Integer) = x + oneunit(x) # Любой целочисленный тип
addone(x::Number) = x + oneunit(x) # Любой числовой тип
addone(x) = x + oneunit(x) # Любой тип, поддерживающий + и единицу
加`!'到改变其参数的函数的名称
而不是:
function double(a::AbstractArray{<:Number})
for i in eachindex(a)
a[i] *= 2
end
return a
end
用这个:
function double!(a::AbstractArray{<:Number})
for i in eachindex(a)
a[i] *= 2
end
return a
end
Julia Base在任何地方都使用此约定,并包含同时具有复制和更改表单的函数示例(例如, 排序'
和 '排序!),以及仅改变的函数(例如, '推!, '啪!, '剪接!`). 为了方便起见,此类函数可以返回修改后的数组。
与I/O或使用随机数生成器(rng)相关的函数是重要的例外:由于这些函数几乎总是应该修改I/O或RNG数据,因此以`!用于指示I/O数据的修改或RNG状态的提升以外的变化。 例如,'rand(x)`修改RNG,而`rand!(x)`同时修改RNG和’x'。 同样’read(io)`修改`io
,而`read!(io,x)'修改两个参数。
使用导出的方法,而不是直接访问字段。
惯用的Julia代码通常应该将模块的导出方法视为其类型的接口。 对象字段通常被认为是实现细节,只有当它被声明为API时,用户代码才应该直接访问它们。 这有几个优点。
-
包开发人员可以更自由地更改实现,而不会破坏用户的代码。
-
方法可以在更高阶的构造中传递,例如 'map'(例如,'map(imag,zs)`),而不是'[z.im 为zs中的z]'。
-
可以基于抽象类型定义方法。
-
方法可以描述可以对不同类型通用的概念操作(例如,'real(z)`使用复数或四元数)。
Julia的调度系统支持这种风格,因为’play(x::MyType)`只为这个特定的类型定义’play`方法,让其他类型有自己的实现。
此外,非导出函数通常是内部的,除非文档中另有说明,否则可以更改。 名称有时以`_'作为前缀(或后缀),以额外指示某些部分是内部或实现细节,但这不是规则。
以下是此规则的反例: '命名', 'RegexMatch', 'StatStruct'。
使用与Julia’base’约定的命名约定
-
模块和类型的名称使用大写字母和camelCase’module SparseArrays','struct UnitRange'。
-
小写用于函数('最大值`, 'convert')并且,当它们可读时,它们的名字中的几个词被写在一起('等效`, 'haskey')。 如有必要,请使用下划线作为单词分隔符。 下划线也用于表示概念的组合('remotecall_fetch'作为’fetch(remotecall(…))+`)或作为修饰符。
-
至少改变其中一个参数的函数以'!`.
-
简洁是赞赏,但避免缩写('indexin',而不是’indxin`),因为很难记住特定单词是否缩写,如果是的话,究竟如何。
如果一个函数名需要几个词,请考虑它是否可以代表多个概念,以及将其分成几个部分是否会更好。
使用类似于Julia Base的参数顺序编写函数。
通常,基库根据具体情况使用以下函数参数顺序。
-
函数参数。 首先放置函数参数允许使用关键字块。 `do'用于传递多行匿名函数。
-
输入/输出流。 首先指定’IO’对象允许将函数传递给函数,例如
'sprint`,例如’sprint(show,x)'。
-
可变输入数据。 例如,在函数 '填充!(x,v)``x`是一个可变对象,它出现在要插入`x’的值之前。
-
类型。 传递类型通常意味着输出将具有给定类型。 在功能 'parse(Int,"1")
类型位于分析的字符串之前。 有许多类似的例子,其中类型首先出现,但值得注意的是,在 'read(io,String)`IO’参数出现在类型之前,它对应于这里描述的顺序。
-
不可变的输入数据。 在’填充!'function(x,v)’v'_不会改变,在`x’之后。
-
钥匙。 对于关联集合,这是键值对的键。 对于其他索引集合,这是索引。
-
值。 对于关联集合,这是键值对的值。 在像 '填充!(x,v)`,这是`v'。
-
其他。 所有其他论点。
-
可变参数数。 这适用于可以在函数调用结束时无限枚举的参数。 例如,在'矩阵{T}(undef,dims)'测量值可以指定为
-
命名参数。 在Julia中,命名参数在函数定义中应该始终排在最后。 这里给出它们是为了完整。
绝大多数函数不会接受任何列出的参数。 数字只是表示所有适用函数参数必须遵守的优先级。
当然,也有一些例外。 例如,在函数 `convert'类型必须始终是第一个。 在功能 'setindex!'值位于索引之前,因此索引可以作为可变数量的参数提供。
如果您在开发API时尽可能严格地遵守此一般顺序,那么您的函数的用户很可能会收到更一致的界面。
不要过度使用'。..`
以这种方式指定函数参数可能会上瘾。 而不是'[a。..,乙...],只需使用
[a;b]',它已经连接数组。 方法 'collect(a)更好+[a。..]+`,但是由于’a’已经是可迭代的,所以通常最好不要触摸它,不要将其转换为数组。
确保构造函数返回其类型的实例。
当为类型’T`调用方法`T(x)'时,通常期望返回类型T的值。 构造函数可能会导致混乱和不可预测的行为。
julia> struct Foo{T}
x::T
end
julia> Base.Float64(foo::Foo) = Foo(Float64(foo.x)) # Не определяйте методы подобным образом
julia> Float64(Foo(3)) # Должно возвращаться `Float64`
Foo{Float64}(3.0)
julia> Foo{Int}(x) = Foo{Float64}(x) # Не определяйте методы подобным образом
julia> Foo{Int}(3) # Должно возвращаться `Foo{Int}`
Foo{Float64}(3.0)
为了保持代码清晰并确保类型一致性,请始终设计构造函数,以便它们返回应该创建的类型的实例。
不要使用不必要的静态参数。
函数签名:
foo(x::T) where {T<:Real} = ...
它应该以以下格式编写:
foo(x::Real) = ...
特别是如果函数体中没有使用’T'。 即使使用’T`,也可以用函数替换 'typeof(x)`,如果方便的话。 这不会以任何方式影响性能。 请注意,这不是关于静态参数的一般警告,而只是关于在不需要它们的地方使用它们的评论。
另请注意,容器类型可能需要函数调用中的类型参数。 有关详细信息,请参阅 避免使用抽象容器的字段。
避免"类型盗版"
"类型盗版"是指为您没有定义的类型扩展或重新定义Base或其他包中的方法的做法。 在极端情况下,这可能会导致Julia崩溃(例如,如果扩展或重新定义您的方法导致无效的输入数据传递给`ccall')。 类型盗版会使代码正确性的合理性复杂化,并导致难以预测和诊断的不兼容性。
例如,假设您要在模块中定义字符乘法。
module A
import Base.*
*(x::Symbol, y::Symbol) = Symbol(x,y)
end
问题是现在使用`Base’的任何其他模块。*,也会看到这个定义。 由于符号(`Symbol')在Base中定义并被其他模块使用,因此这可能会意外地改变不相关代码的行为。 这里有几种选择,包括使用不同的函数名或将字符(`Symbol
)括在您定义的不同类型中。
有时,相关包可能会进行类型盗版,以将函数与定义分开,特别是如果包是由协作作者开发的,并且定义可以重用的话。 例如,一个包可能提供一些对处理颜色有用的类型;另一个包可能为这些类型定义允许在颜色空间之间转换的方法。 另一个例子是一个包充当一些C代码的薄包装器,然后另一个包可以盗版以实现适用于Julia的更高级别的API。
不要为命名函数`f`编写简单的匿名函数`x->f(x)`
由于高阶函数通常使用匿名函数调用,因此很容易得出结论,这是可取的,甚至是必要的。 但是任何函数都可以直接传递,而不被封闭在匿名函数中。 而不是写`map(x->f(x),a),写 '地图(f,a)
:
如果可能,请避免在泛型代码中对数字文字使用浮点数。
如果您正在编写处理数字的通用代码,并且可以预期使用大量不同的数字类型参数,请尝试使用数值类型文字,这些文字在推进时会对参数产生最小影
例子::
julia> f(x) = 2.0 * x
f (generic function with 1 method)
julia> f(1//2)
1.0
julia> f(1/2)
1.0
julia> f(1)
2.0
鉴于:
julia> g(x) = 2 * x
g (generic function with 1 method)
julia> g(1//2)
1//1
julia> g(1/2)
1.0
julia> g(1)
2
正如你所看到的,在第二个版本中,我们使用了"Int",输入参数的类型被保留了,但在第一个版本中没有。 发生这种情况是因为,例如,'promote_type(Int,Float64)==Float64`,并且在乘法时,执行提升。 同样,文字 'Rational'对类型的破坏性低于字面量 'Float64',但比`Int’更具破坏性。
julia> h(x) = 2//1 * x
h (generic function with 1 method)
julia> h(1//2)
1//1
julia> h(1/2)
1.0
julia> h(1)
2//1
因此,如果可能的话,使用文字’Int’与'Rational{Int}'为字面非整数,以简化代码使用。