Engee 文档

风格指南

以下部分解释了Julia惯用代码编写风格的一些方面。 这些规则都不是绝对的;这些只是指导方针,将帮助您熟悉语言并选择替代开发选项之一。

缩进

每个缩进级别使用4个空格。

编写函数,而不仅仅是脚本。

在顶层的一系列步骤中编写代码是开始解决问题的快速方法,但您应该尝试尽可能快地将程序划分为函数。 函数更好地可重用和可测试,以及明确执行哪些操作以及它们的输入和输出是什么。 此外,由于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)             # Любой тип, поддерживающий + и единицу

'Addone’的最后一个定义处理支持的任何类型 'oneunit'(它在与`x’相同的类型中返回1,这避免了不需要的类型提升)和函数 `+'与这些参数。 重要的是要明白,只定义通用的"addone(x)=x+oneunit(x)"并不会降低性能,因为Julia会根据需要自动编译专用版本。 例如,当第一次调用`addone(12)`函数时,Julia将自动为`x::Int`参数编译专门的`addone`函数,而`oneunit`函数调用将被内置值`1’替换。 因此,上面给出的’addone`的前三个定义完全重复了第四个定义。

在调用端处理过多的参数。

而不是:

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')。

这里的一个问题是,如果一个函数固有地需要整数,那么调用者自己决定应该如何转换非整数可能会更好(例如,使用floor或ceiling方法)。 另一个问题是,声明更具体的类型为将来的方法定义留下了更多的空间。

加`!'到改变其参数的函数的名称

而不是:

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)'修改两个参数。

避免奇怪的类型组合

类型如’Union{Function,AbstractString}",往往是一个标志,建筑可以更准确。

避免复杂的容器类型

按如下方式构建数组通常不是很方便。

a = Vector{Union{Int,AbstractString,Tuple,Array}}(undef, n)

在这种情况下,最好使用'Vector'。{Any}(undef,n)'。 此外,编译器注释特定用例(例如,a[i]::Int)比尝试将许多备选方案打包成一种类型更有用。

使用导出的方法,而不是直接访问字段。

惯用的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的参数顺序编写函数。

通常,基库根据具体情况使用以下函数参数顺序。

  1. 函数参数。 首先放置函数参数允许使用关键字块。 `do'用于传递多行匿名函数。

  2. 输入/输出流。 首先指定’IO’对象允许将函数传递给函数,例如

    'sprint`,例如’sprint(show,x)'。

  3. 可变输入数据。 例如,在函数 '填充!(x,v)``x`是一个可变对象,它出现在要插入`x’的值之前。

  4. 类型。 传递类型通常意味着输出将具有给定类型。 在功能 'parse(Int,"1")类型位于分析的字符串之前。 有许多类似的例子,其中类型首先出现,但值得注意的是,在 'read(io,String)`IO’参数出现在类型之前,它对应于这里描述的顺序。

  5. 不可变的输入数据。 在’填充!'function(x,v)’v'_不会改变,在`x’之后。

  6. 钥匙。 对于关联集合,这是键值对的键。 对于其他索引集合,这是索引。

  7. 。 对于关联集合,这是键值对的值。 在像 '填充!(x,v)`,这是`v'。

  8. 其他。 所有其他论点。

  9. 可变参数数。 这适用于可以在函数调用结束时无限枚举的参数。 例如,在'矩阵{T}(undef,dims)'测量值可以指定为

    '元组',例如'矩阵{T}(undef,(1,2))',或作为 Vararg's,例如'+矩阵{T}(1,1,2)+

  10. 命名参数。 在Julia中,命名参数在函数定义中应该始终排在最后。 这里给出它们是为了完整。

绝大多数函数不会接受任何列出的参数。 数字只是表示所有适用函数参数必须遵守的优先级。

当然,也有一些例外。 例如,在函数 `convert'类型必须始终是第一个。 在功能 'setindex!'值位于索引之前,因此索引可以作为可变数量的参数提供。

如果您在开发API时尽可能严格地遵守此一般顺序,那么您的函数的用户很可能会收到更一致的界面。

不要过度使用异常处理

避免错误比尝试拦截错误更好。

不要把条件放在括号里。

在Julia中,不需要将`if`和`while’中的条件括在括号中。 写:

if a == b

而不是:

if (a == b)

不要过度使用'。..`

以这种方式指定函数参数可能会上瘾。 而不是'[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)`,如果方便的话。 这不会以任何方式影响性能。 请注意,这不是关于静态参数的一般警告,而只是关于在不需要它们的地方使用它们的评论。

另请注意,容器类型可能需要函数调用中的类型参数。 有关详细信息,请参阅 避免使用抽象容器的字段

避免混淆某些东西是实例还是类型。

像下面这样的定义集令人困惑。

foo(::Type{MyType}) = ...
foo(::MyType) = foo(MyType)

决定有问题的概念是写成’MyType’还是’MyType()',并坚持下去。

首选样式是使用默认实例,并添加包含`Type'的方法。{MyType}如果需要解决一些问题,应该在以后添加`。

如果类型实际上是枚举,则应将其定义为单个类型(理想情况下是不可变结构类型或基元类型),并且枚举值应为其实例。 您可以使用构造函数和转换检查值的有效性。 这种构造比枚举成为抽象类型,值成为子类型的构造更可取。

不要过度使用宏

请记住宏实际上可以是函数的情况。

函数调用 `eval'宏内部是一个特别危险的警告信号。 这意味着宏只有在顶层调用时才会工作。 如果这样的宏被写成一个函数,它自然就可以访问它需要的运行时值。

不要在接口级别使用不安全的操作。

如果你有一个使用自己的指针的类型:

mutable struct NativeType
    p::Ptr{UInt8}
    ...
end

不要编写如下定义:

getindex(x::NativeType, i) = unsafe_load(x.p, i)

问题是这种类型的用户可以写入’x[i]'而没有意识到这种操作是不安全的,然后会容易出现内存错误。

这样的功能应该检查操作以确保其安全,或者在其名称的某个地方有"不安全"以警告呼叫方。

不要重载基本容器类型的方法。

您可以编写如下定义。

show(io::IO, v::Vector{MyType}) = ...

在这种情况下,将显示具有特定新元素类型的向量。 虽然看起来很诱人,但应该避免这种情况。 问题是用户期望像`Vector()`这样的知名类型以某种方式表现,并且过度调整其行为会使其难以使用。

避免"类型盗版"

"类型盗版"是指为您没有定义的类型扩展或重新定义Base或其他包中的方法的做法。 在极端情况下,这可能会导致Julia崩溃(例如,如果扩展或重新定义您的方法导致无效的输入数据传递给`ccall')。 类型盗版会使代码正确性的合理性复杂化,并导致难以预测和诊断的不兼容性。

例如,假设您要在模块中定义字符乘法。

module A
import Base.*
*(x::Symbol, y::Symbol) = Symbol(x,y)
end

问题是现在使用`Base’的任何其他模块。*,也会看到这个定义。 由于符号(`Symbol')在Base中定义并被其他模块使用,因此这可能会意外地改变不相关代码的行为。 这里有几种选择,包括使用不同的函数名或将字符(`Symbol)括在您定义的不同类型中。

有时,相关包可能会进行类型盗版,以将函数与定义分开,特别是如果包是由协作作者开发的,并且定义可以重用的话。 例如,一个包可能提供一些对处理颜色有用的类型;另一个包可能为这些类型定义允许在颜色空间之间转换的方法。 另一个例子是一个包充当一些C代码的薄包装器,然后另一个包可以盗版以实现适用于Julia的更高级别的API。

小心类型相等。

通常,您需要使用函数来测试类型。 'isa''<:',而不是`=='。 检查类型是否完全相等通常只有在与已知的特定类型(例如,T==Float64)进行比较时才有意义,或者如果您真的知道自己在做什么。

<无翻译>

不要为命名函数`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}'为字面非整数,以简化代码使用。