Kotlin 的包保护替代品

在 Java 中,我们为类提供了包 protected (默认)修饰符,它允许我们在一个包中包含多个类,但只公开少数类,并保持逻辑封装。

对 Kotlin 而言,情况似乎并非如此。如果我想要一些类彼此可见,但不能更进一步,我必须使用一个私有修饰符来限制单个文件的可见性。

因此,如果你想在一个包中有10个类,但是只有一个是公共的,你就必须有一个包含所有类的大文件(private到处都是)。

这是正常的做法,还是 Kotlin 有办法实现类似的模块化?

我不明白: 如果他们有包的概念,为什么他们要放弃包保护访问?

更新: 我们可能有包保护的可见性毕竟
请看这里的讨论

更新: 如果您通读了讨论,仍然认为这是语言的必备功能,请投票 给你

29630 次浏览

Kotlin, compared to Java, seems to rely on packages model to a lesser degree (e.g. directories structure is not bound to packages). Instead, Kotlin offers internal visibility, which is designed for modular project architecture. Using it, you can encapsulate a part of your code inside a separate module.

So, on top level declarations you can use

  • private to restrict visibility to the file
  • internal to restrict visibility to the module

At this point, there is no other option for visibility restriction.

As @hotkeys points out, you can use the internal keyword in a module or you can put all classes that would otherwise belong in a package inside a single file, but sticking several classes in a file may be a questionable design decision.

For me, the package visibility is helpful for its documenting value. I want to know what public interface some package is presenting to the rest of the project, hide factory implementation classes and so on.

So even if it's possible to access package-private classes and methods in Java, I still choose to use the package modifier.

For this I created a project with a single annotation:

package com.mycompany.libraries.kotlinannotations;


import static java.lang.annotation.ElementType.CONSTRUCTOR;
import static java.lang.annotation.ElementType.METHOD;
import static java.lang.annotation.ElementType.TYPE;
import static java.lang.annotation.RetentionPolicy.SOURCE;


import java.lang.annotation.Documented;
import java.lang.annotation.Retention;
import java.lang.annotation.Target;


@Documented
@Retention(SOURCE)
@Target({ TYPE, METHOD, CONSTRUCTOR })
/**
* Use in Kotlin code for documentation purposes.
*
* Whenever a Kotlin class or method is intended to be accesible at package level only.
*
*/
public @interface PackagePrivate {


}

Then I can use this annotation in any Kotlin project.

The second step, which I haven't done yet, is creating a PMD rule to enforce this with maven (or any other build tool for that matter) and also be able to see violations of the rule in my IDE with the pmd plugin.

There no is full Kotlin support in pmd at this moment but it seems to be expected at some point.

As a workaround for me on android I've created @PackagePrivate annotation and lint checks to control access. Here you can find the project.

Lint checks are obviously not that strict as compiler checks and some setup needed to fail the build on errors. But android studio picks up lint checks automatically and shows error immediately while typing. Unfortunately I don't know a way to exclude annotated members from autocomplete.

Also, as lint is a purely compile-time tool, no checks at runtime performed.

Package-based protection is pointless in Kotlin because packages themselves are unprotected

In Java, package was tied to directory structure. So if you put your classes in com\example\yoursecretengine, any attempt (deliberate or accidental) to add a rogue class there would be easily noticeable. This is the kind of security we've depended on.

Kotlin removes the ties between directory and package, so I can put my class in "my" directory (eg.src\java\pl\agent_l\illegalaccess) yet declare its package as com.example.yoursecretengine - and gain access to all the properties you've meant as package private.

In fact, a Kotlin project works perfectly without ANY package declarations. This only highlights that packages are "more what you'd call guidelines than actual rules". They're a convenience feature, useful only to unclutter namespace and nothing more.

Relevant quotes from kotlinlang:

unlike many other languages, Kotlin packages do not require files to have any specific locations w.r.t. itself; the connection between a file and its package is established only via a package header.

And:

an absence of a package header in a file means it belongs to the special root package.

A near-replacement for package private visibility is available using the opt-in requirements feature (credit to pdvrieze on Kotlin discussions). This is the annotation syntax typically used to flag experimental features in an API.

To use it, create an annotation denoting package private declarations:

@RequiresOptIn(message = "Only to be used in MyPackage")
@Retention(AnnotationRetention.BINARY)
annotation class MyPackagePrivate

Then annotate any methods you want to be package private with it:

@MyPackagePrivate
fun aPackagePrivateMethod() {
// do something private within a package
}

In this way a warning will be generated on any method that calls the annotated method unless the calling method is itself annotated with the corresponding @OptIn annotation, here shown at class level:

@OptIn(MyPackagePrivate::class)
class AClassInThePackage {
fun userOfPackagePrivateMethod() {
aPackagePrivateMethod()
}
}

This, then, produces a similar effect to Java's package private, except that calling methods need to explicitly opt in to using a package private declaration.

If it is desired to generate an error rather than a warning, the level parameter of @RequiresOptIn can be specified:

@RequiresOptIn(level = RequiresOptIn.Level.ERROR, message = "Only to be used in MyPackage")
// annotation declaration as before