AnyMath 文档

风格指南

以下部分解释了惯用Julia编码风格的几个方面。 这些规则都不是绝对的;它们只是帮助您熟悉语言并帮助您在替代设计中进行选择的建议。

缩进

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

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

在顶层编写代码作为一系列步骤是开始解决问题的快速方法,但您应该尝试尽快将程序划分为函数。 函数更具可重用性和可测试性,并阐明正在执行的步骤以及它们的输入和输出是什么。 此外,由于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

最后的定义 阿登,阿登 处理任何类型的支持 一个单位(返回1的类型与 x,从而避免了不需要的类型提升)和 +函数与这些参数。 要意识到的关键是,定义_only_一般是没有性能惩罚的 addone(x)=x+oneunit(x),因为Julia会根据需要自动编译专门的版本。 例如,你第一次打电话 插件(12),Julia会自动编译一个specialized 阿登,阿登 功能为 x::Int 参数,调用 一个单位 由其内联值替换 1. 因此,前三个定义 阿登,阿登 以上是完全多余的第四个定义。

处理调用方中多余的参数多样性

而不是:

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] &ast;= 2
    end
    return a
end

使用方法:

function double!(a::AbstractArray{<:Number})
    for i in eachindex(a)
        a[i] &ast;= 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) 改变两个参数。

避免奇怪的类型 工会s

类型如 工会{Function,AbstractString} 通常是一些设计可以更干净的标志。

避免复杂的容器类型

像下面这样构造数组通常没有多大帮助:

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的参数排序编写函数

作为一般规则,基库对函数使用以下参数顺序(如适用):

  1. *函数参数*。 首先放置一个函数参数允许使用 传递多行匿名函数的块。

  2. *I/O流*。 指定 伊俄 对象首先允许将函数传递给函数,例如 冲刺,例如 冲刺(显示,x).

  3. *输入正在突变*。 例如,在 填满!(x,v), x 对象是否正在突变,并且它出现在要插入的值之前 x.

  4. *类型*。 传递类型通常意味着输出将具有给定的类型。 在 解析(Int,"1"),类型在要解析的字符串之前。 有很多这样的例子,其中的类型首先出现,但它是有用的注意,在 读取(io,字符串),该 伊俄 参数出现在类型之前,这与这里列出的顺序一致。

  5. *输入未突变*。 在 填满!(x,v), v 是_not_被突变,它来后 x.

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

  7. *值*。 对于关联集合,这是键值对的值。 在这样的情况下 填满!(x,v),这是 v.

  8. *其他一切*。 任何其他论点。

  9. *Varargs*。 这是指可以在函数调用结束时无限期列出的参数。 例如,在 矩阵{T}(undef,dims),尺寸可以给出为 元组,例如 矩阵{T}(undef,(1,2)),或作为 瓦拉格s,例如 矩阵{T}(undef,1,2).

  10. *关键字参数*。 在Julia中,关键字参数在函数定义中必须排在最后;为了完整起见,它们在这里列出。

绝大多数函数不会采用上面列出的所有类型的参数;数字只是表示应该用于函数的任何适用参数的优先级。

当然也有一些例外。 例如,在 转换/转换,类型应该永远是第一位的。 在 setindex!,该值位于索引之前,以便索引可以作为varargs提供。

在设计Api时,尽可能遵循此一般顺序可能会为您的函数的用户提供更一致的体验。

不要过度使用try-catch

避免错误比依靠捕捉错误更好。

不要用括号表示条件

朱莉娅不需要周围条件的parens 如果. 写:

if a == b

而不是:

if (a == b)

不要过度使用 ...

拼接函数参数可能会让人上瘾。 而不是 [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.&ast;
&ast;(x::Symbol, y::Symbol) = Symbol(x,y)
end

问题是,现在使用的任何其他模块 基地。* 也会看到这个定义。 自 符号 在Base中定义并被其他模块使用,这可能会意外改变不相关代码的行为。 这里有几种选择,包括使用不同的函数名,或包装 符号s在您定义的另一种类型中。

有时,耦合的包可能会进行类型盗版,以将特性与定义分开,特别是当包是由协作作者设计的,并且定义是可重用的时。 例如,一个包可能提供一些用于处理颜色的类型;另一个包可以为那些类型定义方法,以便在颜色空间之间进行转换。 另一个例子可能是一个包,它充当一些C代码的薄包装器,然后另一个包可能会盗版以实现更高级别的,Julia友好的API。

小心类型相等

您通常希望使用 伊萨<:对于测试类型,而不是 ==. 检查类型是否完全相等通常只有在与已知的具体类型进行比较时才有意义(例如 T==Float64),或者如果你真的知道你在做什么。

不要写一个微不足道的匿名函数 x->f(x) 对于命名函数 f

由于高阶函数通常与匿名函数一起调用,因此很容易得出结论,这是可取的,甚至是必要的。 但是任何函数都可以直接传递,而不会被匿名函数"包装"。 而不是写作 地图(x->f(x),a),写 地图(f,a).

尽可能避免在泛型代码中为数字文字使用浮点数

如果您编写处理数字的泛型代码,并且可以期望使用许多不同的数字类型参数运行,请尝试使用数字类型的文字,这些文字将通过提升尽可能少地影响

例如,

julia> f(x) = 2.0 &ast; 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 &ast; 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 &ast; 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} 对于字面的非整数,为了更容易使用你的代码。