Kotlin中的常量——创建它们的推荐方式是什么?

371

如何在 Kotlin 中创建常量并使用什么命名规范?我在文档中没有找到相关内容。

companion object {
    //1
    val MY_CONST = "something"

    //2
    const val MY_CONST = "something"

    //3
    val myConst = "something"
}

还是……呢?


11
如果你想要一个对应于Java中的public static final字段的东西,在你的伴生对象中使用const val。如果你想要一个private static final字段和一个公共getter,就在你的伴生对象中使用val - Michael
8
这是一篇博客文章,介绍了在 Kotlin 中定义常量的方法:https://blog.egorand.me/where-do-i-put-my-constants-in-kotlin/ - Micer
1
请查看这篇文章。它很好地概述了存储常量的不同方式以及相关的性能权衡。 - firedrillsergeant
1
一眼看出,Val vs Const - Anoop M Maddasseri
1
https://kotlinlang.org/docs/coding-conventions.html#property-names - k4dima
15个回答

336

避免使用伴生对象。在底层,会为可访问的字段创建getter和setter实例方法。调用实例方法在技术上比调用静态方法更昂贵。

public class DbConstants {
    companion object {
        val TABLE_USER_ATTRIBUTE_EMPID = "_id"
        val TABLE_USER_ATTRIBUTE_DATA = "data"
    }

请将常量定义在 object 中。

推荐做法:

object DbConstants {
    const val TABLE_USER_ATTRIBUTE_EMPID = "_id"
    const val TABLE_USER_ATTRIBUTE_DATA = "data"
}

您可以像这样全局访问它们: DbConstants.TABLE_USER_ATTRIBUTE_EMPID


4
@ErwinBolwidt,我认为@sudesh的观点是,当结构的唯一目的是为一些常量值提供命名空间时,不应使用类包装伴生对象设计。但如果您的结构需要可实例化并且还要封闭一些const val,则声明一个companion object是正确的。 - Ari Lacenski
17
@ErwinBolwidt: Sudesh是正确的,为伴生对象生成字节码会在底层涉及额外的对象创建和getter。要了解更多通过反编译Kotlin代码示例进行良好解释,请参见https://blog.egorand.me/where-do-i-put-my-constants-in-kotlin/。 - dominik
2
感谢@dominik,这是一篇非常详细的文章,我推荐给想深入了解此内容的所有人。 Kotlin 产生次优字节码的情况很多,JetBrains 已经解决了许多与性能相关的错误...请关注 https://discuss.kotlinlang.org/,您将了解到许多这样的基本方面。 - Extremis II
7
伴生对象也是一个对象。不确定为什么这个观点得到了高票支持。技术上来说,如果你想创建一个单例,你可以将类转换为对象。但这可能会传达“根本不使用伴生对象”的错误观念。事实上,在伴生对象中也可以使用const val - EpicPandaForce
5
对于那些对文章感兴趣但无法访问@dominik上面的链接的人,这里是有效的链接:https://www.egorand.dev/where-should-i-keep-my-constants-in-kotlin/ - Ivan Agrenich
显示剩余7条评论

188
在Kotlin中,如果你想要创建局部常量并且只在类内使用,可以按照以下方式创建:

val MY_CONSTANT = "Constants"

如果你想在Kotlin中创建一个像Java中的public static final这样的公共常量,你可以按以下方式创建:

companion object {    
    const val MY_CONSTANT = "Constants"    
}

3
我该如何将它用于另一个文件,例如一个名为“Constants.kt”的新文件中? - Naveed Abbas
2
我使用一个文件来存储常量,将所有的常量都放在那里。 - filthy_wizard
4
不需要companion object。我认为@piotrpo的答案应该被采纳。 - Chiara
2
@Chiara 伴生对象(以及其封装的类)作为命名空间,而不是顶层声明。我认为这两个答案都有可能在不同情况下有意义。 - jingx
5
在类的字段中使用名称 MY_CONSTANT 实际上不是一个好主意 - 编译器会通过消息 “私有属性名'MY_CONSTANT'不应包含中间或结尾的下划线” 进行警告。这违反了推荐的命名规范。 - Daniel
@cris 在我看来,使用伴生对象(companion object)const val来定义常量是最佳实践。 - Daniel

120

首先,Kotlin中常量的命名规则与Java相同(例如:MY_CONST_IN_UPPERCASE)。

我该如何创建它?

1. 作为顶层值(推荐)

您只需将常量放在类声明之外即可。

两种可能性:在您的类文件中声明常量(您的常量与您的类具有明显的关系)

private const val CONST_USED_BY_MY_CLASS = 1

class MyClass { 
    // I can use my const in my class body 
}

创建一个专用的constants.kt文件,用于存储全局常量(在项目中广泛使用您的常量):


package com.project.constants
const val URL_PATH = "https:/"

然后您只需在需要它的地方导入它:

import com.project.constants

MyClass {
    private fun foo() {
        val url = URL_PATH
        System.out.print(url) // https://
    }
}

2. 在伴生对象(或对象声明)中声明它

这样做会生成一个无用的对象,因此这种方法不够优雅

MyClass {
    companion object {
        private const val URL_PATH = "https://"
        const val PUBLIC_URL_PATH = "https://public" // Accessible in other project files via MyClass.PUBLIC_URL_PATH
    }
}

如果你将其声明为val而不是const(编译器将生成一个无用的对象+一个无用的函数),情况会更糟:

更糟的情况是,如果你将其声明为val而不是const(编译器将生成一个无用的对象 + 一个无用的函数):

MyClass {
    companion object {
        val URL_PATH = "https://"
    }
}

注意:

Kotlin中的const只能保存原始类型。如果要将其赋值为调用函数的结果,则需要添加@JvmField注释。在编译时,它将被转换为公共静态常量变量。但是与基本类型相比速度较慢。请尽量避免使用。

@JvmField val foo = Foo()

2
这应该是被接受的答案。无论如何,在像这样的情况下: public static final Pattern REGEX_NOTEMPTY = Pattern.compile(".+") ???? - Alessandro Scarozza
这个“推荐”在哪里?有官方 Kotlin 资源的链接吗? - treesAreEverywhere
当我像你建议的那样在类定义上方执行 private const val CONST_USED_BY_MY_CLASS = 1 操作时,Android Studio 4.1.1 开始在项目树中将我添加此行的 *.kt 文件显示为“未解决”。这对编译没有任何影响,但一旦我注意到它,就变得非常烦人。希望很快能够修复。 - artman
@treesAreEverywhere 请参考 https://kotlinlang.org/docs/coding-conventions.html#property-names :“常量的名称...应该使用大写下划线分隔的名称”。 - simpleuser

32

在编译时已知的值可以(而且我认为应该)标记为常量。

命名规范应该遵循Java的规定,并且在从Java代码中使用时应该正确可见(尽管使用伴生对象有些困难,但无论如何)。

适当的常量声明如下:

const val MY_CONST = "something"
const val MY_INT = 1

5
为什么命名约定应该遵循Java的惯例? - Jodimoro
7
Kotlin通常默认遵循Java规范,以使交互操作更加平稳,除非另有说明。 - zsmb13
5
在文档@Jodimoro http://kotlinlang.org/docs/reference/coding-conventions.html中指定了这样的方式。 - the-ginger-geek
3
@Neil,不是这样的。 - Jodimoro
14
在我发布的链接中,他们说:“如果有疑问,请遵循Java编码规范。” - the-ginger-geek
显示剩余2条评论

32

在 Kotlin 中声明常量时,您不需要使用类、对象或伴生对象。您可以只声明一个包含所有常量的文件(例如 Constants.kt),或者将它们放在任何现有的 Kotlin 文件中。直接在文件内部声明常量即可。在编译时已知的常量必须标记为 const

因此,在这种情况下,应该是:

const val MY_CONST = "something"

然后你可以使用以下代码导入常量:

import package_name.MY_CONST

你可以参考这个链接


32
常量必须在它们相关的类中。如果你创建一个“常量”类,最终会有数百个常量在其中。例如:MAX_WIDTH、MAX_HEIGHT 必须在 Screen 类中,这样你才能逻辑上访问它们:Screen.MAX_WIDTH,而不需要输入 Constants.SCREEN_MAX_WIDTH。两年后,Constants.SCR_MAX_W 和 Constants.MAX_WIDTH 可能会重复,因为当人们按 Ctrl+space 自动完成时,没有人会滚动数百/千行。认真对待这个问题:不要这样做,会导致难以维护。 - inigoD
1
@inigoD 如果您只在一个地方或仅在子级中使用常量,则这是正确的,但这几乎从未发生过。如果您将常量放在一个晦涩的类中,那么您可能会忘记它,或者更有可能接管代码库,您可能会重复使用它们。或者不明显应该将它们放在哪里。源或目标?您可以创建多个常量文件,这些文件易于查找。一个用于首选项键,一个用于请求键,一个用于视图常量等。 - Herrbert74
2
@Herrbert74,很抱歉我不同意您的观点。我同意有时可能很难找到常量所属的类,但是应该始终将其放在与之最相关的类中。如果您希望稍后检索它们,则将它们随机保存在随机编号文件中并不是最佳方法...您会争辩说它们不会被随机存储,而是与常量相关的包中,但这只是不将它们放在它们所属的类中的借口,而这才是它们的归属地... - inigoD
6
如果一个常量真正是全局的或者具有较大的范围,例如在所有包中使用的注释值或被多个控制器获取的Header名称,那么完全可以创建一个适当作用域的“常量类”。然而,只在特定上下文中使用的常量应该被限定在该上下文中,并在相关类中声明。 - Nephthys76
@Nephthys76 只是提醒一下,对于“例如在所有包中使用的注释值”,我认为最好的常量位置是在注释类中。 - Slaw

12

如果你把const val valName = valValue放在类名前面,它将创建一个public static final YourClass.Kt,它会有 public static final 的值。

Kotlin:

const val MY_CONST0 = 0
const val MY_CONST1 = 1
data class MyClass(var some: String)

Java反编译:

public final class MyClassKt {
    public static final int MY_CONST0 = 0;
    public static final int MY_CONST1 = 1;
}
// rest of MyClass.java

1
这是真的吗?有人对这种方法有任何经验吗? - SMBiggs

10

val一样,使用const关键字定义的变量是不可变的。这里的区别在于const用于在编译时已知的变量。

声明一个const变量就像在Java中使用static关键字一样。

让我们看看如何在Kotlin中声明一个const变量:

const val COMMUNITY_NAME = "wiki"

Java中编写的类似代码如下:

final static String COMMUNITY_NAME = "wiki";

在上面的回答中补充一点:

@JvmField 可以用来指示 Kotlin 编译器不为此属性生成 getter/setter 并将其公开为字段。

 @JvmField
 val COMMUNITY_NAME = "Wiki"

静态字段

Kotlin属性在命名对象或伴生对象中声明时,将具有静态支持字段,这些字段可以在命名对象或包含伴生对象的类中。

通常这些字段是私有的,但可以通过以下方式之一公开:

  • @JvmField注释;
  • lateinit修饰符;
  • const修饰符。

更多详情请参见https://kotlinlang.org/docs/reference/java-to-kotlin-interop.html#instance-fields


8

本地常量:

const val NAME = "name"

全局常量:

object MyConstants{
    val NAME = "name"
    val ID = "_id"
    var EMAIL = "email"
}

访问MyConstants.NAME


一个问题;是更好地定义一个对象类型,像这样将常量值放在其中,还是创建一个普通类并定义一个包含常量变量的伴生对象。哪种方式应该优先考虑? - Ekin Yücel
1
@EkinYücel,如果你完全没有非静态代码,那么使用object是肯定的。它可以清理导入 - 如果将其放在伴生对象中,则人们必须导入MyConstants.Companion.NAME。此外,您还需要将构造函数标记为私有。这是为了避免使用方便的语言特性而进行的大量工作。 - Hakanai

7

您可以通过几种方式在 Kotlin 中定义常量:

使用伴生对象

    companion object {
        const val ITEM1 = "item1"
        const val ITEM2 = "item2"
    }

你可以在任何类中使用上述伴生对象块,并在此块中定义所有字段。但是这种方法存在一个问题,文档说:
尽管伴生对象的成员看起来像其他语言中的静态成员,在运行时它们仍然是真实对象的实例成员,并且可以实现接口。
当你使用伴生对象创建常量并查看反编译的字节码时,你会看到类似下面的内容:
  ClassName.Companion Companion = ClassName.Companion.$$INSTANCE;
  @NotNull
  String ITEM1 = "item1";
  @NotNull
  String ITEM2 = "item2";
 
  public static final class Companion {
     @NotNull
     private static final String ITEM1 = "item1";
     @NotNull
     public static final String ITEM2 = "item2";
     
     // $FF: synthetic field
     static final ClassName.Companion $$INSTANCE;

     private Companion() {
     }

     static {
        ClassName.Companion var0 = new ClassName.Companion();
        $$INSTANCE = var0;
     }
  }

从这里你可以轻松地看到文档的内容,尽管伴生对象的成员看起来像其他语言中的静态成员,但在运行时它们仍然是真实对象的实例成员。 这做了比所需更多的工作。

现在另一种方法出现了,我们不需要使用伴生对象,像下面这样:

object ApiConstants {
      val ITEM1: String = "item1"
 }

如果你查看上述代码的反编译版本的字节码,你会发现类似这样的内容:

public final class ApiConstants {
     private static final String ITEM1 = "item1";

     public static final ApiConstants INSTANCE;

     public final String getITEM1() {
           return ITEM1;
      }

     private ApiConstants() {
      }

     static {
         ApiConstants var0 = new ApiConstants();
         INSTANCE = var0;
         CONNECT_TIMEOUT = "item1";
      }
    }

现在,如果您查看上面反编译的代码,它为每个变量创建了get方法。这些get方法根本不需要。
为了摆脱这些get方法,您应该在val之前使用const,如下所示,
object ApiConstants {
     const val ITEM1: String = "item1"
 }

现在,如果您查看上面代码段的反编译代码,您会发现它更容易阅读,因为它对您的代码进行了最少的后台转换。
public final class ApiConstants {
    public static final String ITEM1 = "item1";
    public static final ApiConstants INSTANCE;

    private ApiConstants() {
     }

    static {
        ApiConstants var0 = new ApiConstants();
        INSTANCE = var0;
      }
    }

这是创建常量的最佳方式。


在第一个反编译代码中:为什么ITEM1是私有的,而ITEM2是公共的?在第二个反编译代码中:CONNECT_TIMEOUT从哪里来的? - izogfif

6

在任何答案中都没有提到的问题是使用 companion objects 的开销。正如您可以在 这里 阅读到的那样,伴生对象实际上是对象,创建它们需要消耗资源。此外,每次使用常量时,您可能需要经过多个 getter 函数。如果您所需的仅是类的几个实例上的一些原始常量,则最好使用 val 来获得 更好的性能 并避免使用 companion object。这种权衡是如果您有许多类实例,则会导致更高的内存消耗,因此每个人都应该自行决定。

文章的 TL;DR;:

实际上将此 Kotlin 代码使用伴生对象:

class MyClass {

    companion object {
        private val TAG = "TAG"
    }

    fun helloWorld() {
        println(TAG)
    }
}

转化为这段 Java 代码:

public final class MyClass {
    private static final String TAG = "TAG";
    public static final Companion companion = new Companion();

    // synthetic
    public static final String access$getTAG$cp() {
        return TAG;
    }

    public static final class Companion {
        private final String getTAG() {
            return MyClass.access$getTAG$cp();
        }

        // synthetic
        public static final String access$getTAG$p(Companion c) {
            return c.getTAG();
        }
    }

    public final void helloWorld() {
        System.out.println(Companion.access$getTAG$p(companion));
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接