风格指南
以下部分解释了惯用Julia编码风格的几个方面。 这些规则都不是绝对的;它们只是帮助您熟悉语言并帮助您在替代设计中进行选择的建议。
编写函数,而不仅仅是脚本
在顶层编写代码作为一系列步骤是开始解决问题的快速方法,但您应该尝试尽快将程序划分为函数。 函数更具可重用性和可测试性,并阐明正在执行的步骤以及它们的输入和输出是什么。 此外,由于Julia编译器的工作方式,函数内部的代码往往比顶级代码运行得更快。
同样值得强调的是,函数应该接受参数,而不是直接对全局变量进行操作(除了像 圆周率).
避免写过于具体的类型
代码应该尽可能通用。 而不是写作:
Complex{Float64}(x)
最好使用可用的泛型函数:
complex(float(x))
第二个版本将转换 x 到适当的类型,而不是总是相同的类型。
此样式点与函数参数特别相关。 例如,不要将参数声明为类型 Int型 或 Int32如果它真的可以是任何整数,用抽象类型表示 整数. 事实上,在许多情况下,您可以完全省略参数类型,除非需要从其他方法定义中消除歧义,因为 方法;方法 如果传递的类型不支持任何必要的操作,则无论如何都会抛出。 (这被称为https://en.wikipedia.org/wiki/Duck_typing[鸭子打字]。)
例如,考虑函数的以下定义 阿登,阿登 返回一个加它的参数:
addone(x::Int) = x + 1 # works only for Int
addone(x::Integer) = x + oneunit(x) # any integer type
addone(x::Number) = x + oneunit(x) # any numeric type
addone(x) = x + oneunit(x) # any type supporting + and oneunit
处理调用方中多余的参数多样性
而不是:
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))
这是更好的风格,因为 [医]脚 并不真正接受所有类型的数字;它真的需要 Int型 s.
这里的一个问题是,如果一个函数固有地需要整数,那么强制调用者决定应该如何转换非整数(例如地板或天花板)可能会更好。 另一个问题是,声明更具体的类型为未来的方法定义留下了更多的"空间"。
追加 ! 修改其参数的函数的名称
而不是:
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在整个过程中使用此约定,并包含复制和修改表单的函数示例(例如, 排序和 排序!),以及其他只是修改的(例如, xref:base/collections.adoc#Base.push![推!, 爸爸!, 剪接!). 为方便起见,此类函数通常也返回修改后的数组。
与IO相关的函数或使用随机数生成器(rng)是值得注意的例外:由于这些函数几乎总是必须改变IO或RNG,因此以 ! 用于表示突变_other_而不是突变IO或推进RNG状态。 例如, 兰德(x) 突变RNG,而 兰德!(x) 突变RNG和 x;同理, 阅读(io) 变异体 伊俄,而 读吧!(io,x) 改变两个参数。
避免复杂的容器类型
像下面这样构造数组通常没有多大帮助:
a = Vector{Union{Int,AbstractString,Tuple,Array}}(undef, n)
在这种情况下 向量{Any}(undef,n) 更好。 对编译器注释特定用途也更有帮助(例如 A[i]::Int)而不是试图将许多替代品包装成一种类型。
首选导出方法而不是直接字段访问
惯用的Julia代码通常应该将模块的导出方法视为其类型的接口。 对象的字段通常被认为是实现细节,用户代码只能在声明为API时直接访问它们。 这有几个好处:
*包开发人员可以更自由地更改实现,而不会破坏用户代码。
*方法可以传递给高阶结构,如 地图(例如 地图(imag,zs))而不是 [脧锚脧赂z.im 对于zs中的z]).
*可以在抽象类型上定义方法。
*方法可以描述可以跨不同类型共享的概念操作(例如 真实(z) 在复数或四元数上工作)。
朱莉娅的调度系统鼓励这种风格,因为 播放(x::MyType) 只定义 游戏 该特定类型上的方法,让其他类型有自己的实现。
同样,非导出函数通常是内部的,并且可能会发生变化,除非文档另有说明。 有时会给出一个名字 _ 前缀(或后缀)进一步表明某些东西是"内部"或实现细节,但它不是一个规则。
使用与Julia一致的命名约定 基地/
*模块和类型名称使用大写和驼峰大小写: 模块SparseArrays, 结构单位范围.
*函数是小写的(最大值, 转换/转换)并且,当可读时,将多个单词压扁在一起(等效;等效, 哈斯基). 必要时,使用下划线作为单词分隔符。 下划线也用于表示概念的组合(remotecall_fetch作为更有效地实施 fetch(remotecall(...)))或作为改性剂。
*函数至少改变其中一个参数以 !.
*简洁是有价值的,但避免缩写(索引,索引而不是 印新)因为很难记住特定单词是否以及如何缩写。
如果一个函数名需要多个单词,请考虑它是否可能代表多个概念,并且可能更好地拆分成多个部分。
使用类似Julia Base的参数排序编写函数
作为一般规则,基库对函数使用以下参数顺序(如适用):
-
*函数参数*。 首先放置一个函数参数允许使用
做传递多行匿名函数的块。 -
*I/O流*。 指定
伊俄对象首先允许将函数传递给函数,例如冲刺,例如冲刺(显示,x). -
*输入正在突变*。 例如,在
填满!(x,v),x对象是否正在突变,并且它出现在要插入的值之前x. -
*类型*。 传递类型通常意味着输出将具有给定的类型。 在
解析(Int,"1"),类型在要解析的字符串之前。 有很多这样的例子,其中的类型首先出现,但它是有用的注意,在读取(io,字符串),该伊俄参数出现在类型之前,这与这里列出的顺序一致。 -
*输入未突变*。 在
填满!(x,v),v是_not_被突变,它来后x. -
*钥匙*。 对于关联集合,这是键值对的键。 对于其他索引集合,这是索引。
-
*值*。 对于关联集合,这是键值对的值。 在这样的情况下
填满!(x,v),这是v. -
*其他一切*。 任何其他论点。
-
*Varargs*。 这是指可以在函数调用结束时无限期列出的参数。 例如,在
矩阵{T}(undef,dims),尺寸可以给出为元组,例如矩阵{T}(undef,(1,2)),或作为瓦拉格s,例如矩阵{T}(undef,1,2). -
*关键字参数*。 在Julia中,关键字参数在函数定义中必须排在最后;为了完整起见,它们在这里列出。
绝大多数函数不会采用上面列出的所有类型的参数;数字只是表示应该用于函数的任何适用参数的优先级。
在设计Api时,尽可能遵循此一般顺序可能会为您的函数的用户提供更一致的体验。
不要过度使用 ...
拼接函数参数可能会让人上瘾。 而不是 [a...,乙...],使用简单 [a;b],它已经连接数组。 收集(a)比 [a...],但自 a 已经是可迭代的,通常更好的做法是不要将其转换为数组。
确保构造函数返回自己类型的实例
当一个方法 T(x) 被调用的类型 T,一般期望返回一个类型为T的值。 构造函数可能会导致混乱和不可预测的行为:
julia> struct Foo{T}
x::T
end
朱莉娅>基地。Float64(foo::Foo)=Foo(Float64(foo.x))#不要定义这样的方法
julia>Float64(Foo(3))#应该返回 `漂浮64`
[医]脚{Float64}(3.0)
朱莉娅>Foo{Int}(x)=Foo{Float64}(x)#不要这样定义方法
朱莉娅>Foo{Int}(3)#应该返回 `[医]脚{Int}`
[医]脚{Float64}(3.0)
为了保持代码清晰度并确保类型一致性,始终设计构造函数以返回它们应该构造的类型的实例。
不要使用不必要的静态参数
函数签名:
foo(x::T) where {T<:Real} = ...
应写为:
foo(x::Real) = ...
相反,特别是如果 T 在函数体中不使用。 即使 T 被使用,它可以被替换为 类型(x)如果方便的话。 没有性能差异。 请注意,这不是针对静态参数的一般警告,只是针对不需要它们的使用。
还要注意容器类型,具体来说可能需要函数调用中的类型参数。 请参阅常见问题 避免使用抽象容器的字段以获取更多信息。
避免混淆某些东西是实例还是类型
像下面这样的定义集令人困惑:
foo(::Type{MyType}) = ...
foo(::MyType) = foo(MyType)
决定有问题的概念是否会写成 [医]类型 或 MyType(),并坚持下去。
首选样式是默认使用实例,并且只添加涉及 类型{MyType} 后来,如果他们成为必要解决一些问题。
如果一个类型实际上是一个枚举,它应该被定义为一个单一的(理想情况下是不可变的结构或基元)类型,枚举值是它的实例。 构造函数和转换可以检查值是否有效。 这种设计比使枚举成为抽象类型(以"值"为子类型)更可取。
不要过度使用宏
请注意何时宏实际上可能是一个函数。
打电话来 埃瓦尔宏内部是一个特别危险的警告信号;这意味着宏只有在顶层调用时才会工作。 如果这样的宏被写成函数,它自然就可以访问它需要的运行时值。
不要在接口级别公开不安全的操作
如果您有使用本机指针的类型:
mutable struct NativeType
p::Ptr{UInt8}
...
end
不要编写如下定义:
getindex(x::NativeType, i) = unsafe_load(x.p, i)
问题是这种类型的用户可以写 x[i] 而没有意识到操作不安全,进而容易受到内存bug的影响。
这样的功能应该检查操作以确保它是安全的,或者具有 不安全 在它的名字的某个地方提醒呼叫者。
不要重载基本容器类型的方法
可以编写如下定义:
show(io::IO, v::Vector{MyType}) = ...
这将提供具有特定新元素类型的矢量的自定义显示。 虽然诱人,但应该避免这种情况。 麻烦的是,用户会期望一个众所周知的类型,如 向量() 以某种方式行事,并且过度自定义其行为可能会使其更难以使用。
避免类型盗版
"类型盗版"是指在您尚未定义的类型上扩展或重新定义Base或其他包中的方法的做法。 在极端情况下,您可以崩溃Julia(例如,如果您的方法扩展或重新定义导致无效输入传递给 ccall). 类型盗版会使代码推理复杂化,并可能引入难以预测和诊断的不兼容性。
例如,假设您想在模块中的符号上定义乘法:
module A
import Base.*
*(x::Symbol, y::Symbol) = Symbol(x,y)
end
问题是,现在使用的任何其他模块 基地。* 也会看到这个定义。 自 符号 在Base中定义并被其他模块使用,这可能会意外改变不相关代码的行为。 这里有几种选择,包括使用不同的函数名,或包装 符号s在您定义的另一种类型中。
有时,耦合的包可能会进行类型盗版,以将特性与定义分开,特别是当包是由协作作者设计的,并且定义是可重用的时。 例如,一个包可能提供一些用于处理颜色的类型;另一个包可以为那些类型定义方法,以便在颜色空间之间进行转换。 另一个例子可能是一个包,它充当一些C代码的薄包装器,然后另一个包可能会盗版以实现更高级别的,Julia友好的API。
不要写一个微不足道的匿名函数 x->f(x) 对于命名函数 f
由于高阶函数通常与匿名函数一起调用,因此很容易得出结论,这是可取的,甚至是必要的。 但是任何函数都可以直接传递,而不会被匿名函数"包装"。 而不是写作 地图(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
朱莉娅>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型 literal,保留了输入参数的类型,而第一个没有。这是因为例如 promote_type(Int,Float64)==Float64,并促进与乘法发生。 同样地, 理性的文字的类型破坏性小于 xref:base/numbers.adoc#Core.Float64[漂浮64 文字,但比文字更具破坏性。 Int型s:
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型 如果可能的话,文字 理性{Int} 对于字面的非整数,为了更容易使用你的代码。