英文:
Object equality vs uniqueness
问题
在我的 Kotlin 爱好项目中,我遇到了如何在类似以下类的情况下实现 equals() 方法的困境:
// 我使用类似 Kotlin 的语法
class ConfigurationParameter {
val name: String // 用于命令行选项,*.conf 文件参数,环境变量等
val allowedValues: Set<String> // 配置参数的有效值
val description: String // 用于 --help,作为 *.conf 文件中参数上方的注释等
}
相等性
从我的角度来看,只有这个类的两个对象在所有属性上都相等时才相等。否则它们将表现不同:
- 对于名称 ... 那是完全不同的参数。
- 对于 allowedValues ... 验证会不同。
- 对于 description ... 打印的使用帮助会不同。
唯一性
与此同时,我不希望两个对象只有相同的名称(但可能具有不同的 allowedValues 或 description)出现在同一个集合中 (Set<ConfigurationParameter>
)。这将导致诸如重复的命令行选项等问题。
不应该发生这种情况
我知道在应用程序中本应该不会创建两个具有相同名称但具有不同其他属性的配置参数。但让我们将其视为某种内部自检机制。
解决方案
我目前唯一想到的解决方案是创建一个全新的 ConfigurationParameterSet(不基于 Set),它通过其名称而不是通过其 equals() 方法来处理其项目的“相似性”。
这种解决方案的问题在于必须为每个具有不同于其唯一性的相等性的实体类创建一个这样的新 Set 类。
问题
是否有任何已经建立的通用解决方案来解决这种相等性与唯一性的困境?
英文:
In my hobby Kotlin project I've run into dilema how to implement equals() method in case of a class like:
// I'm using Kotlin-like syntax
class ConfigurationParameter {
val name: String // used for command line option, *.conf file parameter, ENV variable, ...
val allowedValues: Set<String> // the valid values of the configuration parameter
val description: String // used in --help, as a comment above parameter in *.conf file, ...
}
Equality
Now, from my POV, two objects of this class are equal only if they are equal in all their properties. Otherwise they would beheave differently:
- In case of name ... that's completely other parameter.
- In case of allowedValues ... the validation would differ.
- In case of description ... the printed usage help would differ.
Uniqueness
At the same time I don't want two objects with just the same name (but possibly with distinct allowedValues or description) to appear in one set (Set<ConfigurationParameter>
).
That would lead to problems like duplicate command line options and the like.
This should not happen
I'm aware of there should not be created two configuration parameters with the same name and distinct other properties in the application in the first place. But let's consider this to be some internal self-check mechanism.
Solution
The only solution I've come at yet is to create a brand new ConfigurationParameterSet (not based on Set) that treats the "sameness" of its items by their name and not by their equals() method.
The problem with this solution is that there must be such a new Set class for every entity class that has equality distinct from its uniqueness.
Question
Is there any well-established generic solution to this equality vs uniqueness dilema?
答案1
得分: 3
不需要代码的部分已翻译如下:
Instead of your custom set-like class, you can use a Map
that uses the name
property as the keys. You could also add extension functions so you can use it kind of like a Set
. In Java, you'd have to extend the class to add these.
如果您有许多类似的类,想要通过 name
属性存储它们,您可以创建一个带有 name
属性的接口,它们都可以使用,然后为使用实现了该接口的值的任何映射创建上述扩展函数:
英文:
Instead of your custom set-like class, you can use a Map
that uses the name
property as the keys. You could also add extension functions so you can use it kind of like a Set
. In Java, you'd have to extend the class to add these.
fun MutableMap<String, ConfigurationParameter>.add(parameter: ConfigurationParameter) =
put(parameter.name, parameter)
fun MutableMap<String, ConfigurationParameter>.remove(parameter: ConfigurationParameter) =
remove(parameter.name, parameter)
operator fun Map<String, ConfigurationParameter>.contains(parameter: ConfigurationParameter) =
containsValue(parameter)
If you have lots of classes like this where you want to store them by a name
property, you could make an interface with a name
property that they can all use and then create the above extension function for any map that uses values that implement the interface:
interface NamedItem { val name: String }
class ConfigurationParameter: NamedItem {
override val name: String,
val allowedValues: Set<String>,
val description: String
}
fun <T: NamedItem> MutableMap<String, T>.add(parameter: T) =
put(parameter.name, parameter)
fun <T: NamedItem> MutableMap<String, T>.remove(parameter: T) =
remove(parameter.name, parameter)
operator fun <T: NamedItem> Map<String, T>.contains(parameter: T) =
containsValue(parameter)
答案2
得分: 1
我不太熟悉Kotlin,但这个问题在Java中听起来完全相同。在Java中,你有两种类型的相等性:(1)引用相等性(a == b),其中a和b都是引用同一个对象的引用,以及(2)hashCode/equals相等性。我怀疑当你谈到"唯一性"时,你不是指引用相等性,而是指一种哈希/equals相等性,其中所有字段都相同。
你遇到的不是语言问题,而是设计问题。你需要决定什么使两个对象相等,或者采取另一种方法。
所以,一种做法是定义一个方法,例如:
enum class Similarity { FULL, NAME }
fun same(other: Any, similarity: Similarity): Boolean
然后,你可以从equals()方法中调用same()来提供默认类型的相似性。你还可以想象将对象设计成一种模态,其中它具有相似性状态,equals方法使用该状态来决定使用哪种类型的相似性。这种状态的缺点是(1)相似性/相等性的关注点不一定由类本身的方法最好定义(关注点分离),以及(2)如果可以避免的话,可变状态不是最佳选择。
另一种可能更好的方法是创建两个Comparator实现,其中一个比较器仅使用名称,另一个比较器使用所有值。这是Java中非常常见的一种方法,在Kotlin中同样容易实现。比较器提供排序顺序,但返回值为0表示相等。如果你更喜欢布尔值,你可以使用相同的技巧,但创建一个接口,例如:
interface SimilarityComparator {
fun same(a: Any, b: Any): Boolean
}
顺便说一下,如果将比较器实现为嵌套类,可以通过省略公开属性值或字段来增加封装性(属性的getter和setter不是很好,参见Alan Holub)。
希望这能帮助你。
Jon
英文:
I'm not well versed in Kotlin, but this problem sounds exactly the same in Java. In Java you have two types of equality: (1) reference equality (a == b) where a and b are both references to the same object and (2) hashCode/equals equality. I suspect when you are talking about "uniqueness" that you don't mean reference equality but rather a notion of hash/equals equality where all fields are the same.
What you have isn't a language problem. It's a design problem. You need to decide what makes two objects equal OR take another approach.
So, one way to do this would be to define a method like:
enum Similarity { FULL, NAME }
boolean same(Object object, Similarity similarity)
Then you can call same() from equals() to give the default kind of similarity. You can also imagine making the object sort of modal, where it has a similarity state and the equals method uses that state to decide which kind of similarity to use. The downside of this state is (1) the concern of similarity/equality isn't necessarily best defined by methods in the class itself (separation of concerns) and (2) mutable state is not the best if you can avoid it.
Another, possibly better, approach might be to create two Comparator implementations, where one comparator uses just the name and the other uses all values. This is a very common approach in Java and should be just as easy in Kotlin. Comparators give sort order, but a return value of 0 indicates equality. If you prefer a boolean, you could use the same technique but create an interface like:
interface SimilarityComparator
{
boolean same(Object a, Object b)
}
BTW, if you implement the comparator as a nested class, you can increase encapsulation by obviating the need to expose property values or fields to allow comparison (property getters and setters are bad, see Alan Holub).
https://www.baeldung.com/java-comparator-comparable
Hopefully this helps.
Jon
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论