Kotlin的默认地图

Kotlin是否有默认地图?

Have you ever used a default map or default dict before? If you know Python a bit, you probably saw its defaultdict in action at some point. Kotlin also comes with a similar tool which I want to demonstrate in this little article.
You can find the Python defaultdict documentation and some examples 这里 但基本用例显示在以下代码段中:

from collections import defaultdict 

d = defaultdict(int)
print(d["someKey"]) //0

The defaultdict can also be used with other types and makes sure that you don't get a KeyError when running your code. Instead, it provides a default value for unknown keys, which can be really helpful for grouping and counting algorithms like the following one:

from collections import defaultdict 

data = [('red', 1), ('blue', 2), ('red', 3), ('blue', 4), ('red', 1), ('blue', 4)]
d = defaultdict(set)
for k, v in data:
    d[k].add(v)

print(d.items()) # dict_items([('red', {1, 3}), ('blue', {2, 4})])

在此帖子中,我想向您展示Kotlin如何处理映射的默认值以及存在的其他备选方案。

一个简单的问题

让我们假设我们想编写一个泛型函数,它采用一个集合,并返回包含与其出现数量相关的原始集合元素的映射:

fun <T> countOccurrences(values: Collection<T>): Map<T, Int>

虽然有(当然)在Kotlin中做到这一点的自然和易于功能的方式,但我们希望首先考虑一些迭代方法。让我们从以下一个开始。

没有默认的迭代解决方案

fun <T> countOccurrences(values: Collection<T>): Map<T, Int> {
    val countMap = mutableMapOf<T, Int>()
    for (e in values) {
        val current = countMap[e]
        if (current == null) countMap[e] = 1 
        else countMap[e] = current + 1
    }
    return countMap
}

We can use a MutableMap for this algorithm. When iterating all values, we look into the map to see whether it has been seen before. If so, we increment the counter; otherwise, we put an initial count of 1 into the map. A similar, but improved, solution is shown in the next part.

具有明确默认值的迭代解决方案

//sampleStart
countMap.putIfAbsent(e, 0)
// getValue, as an alternative to index operator, assures that a non-nullable type is returned
countMap[e] = countMap.getValue(e) + 1 

As an alternative to the previous solution, we can make use of putIfAbsent to ensure that the element is initialized with 0 so that we can safely increment the counter in the following line. What we do in this case is providing an explicit default value. Another similar tool is getOrDefault:

countMap[e] = countMap.getOrDefault(e, 0) + 1

Providing explicit defaults via putIfAbsent or getOrDefault leads to easy and understandable solutions, but we can do better.

隐式默认值的迭代解决方案

Similar to Python's defaultdict, Kotlin comes with a neat 扩大 called MutableMap::withDefault and it looks like this:

fun <K, V> MutableMap<K, V>.withDefault(defaultValue: (key: K) -> V): MutableMap<K, V>

此扩展名让我们为没有在地图中具有关联值的键提供初始化程序。让我们在解决方案中使用它:

fun <T> countOccurrences(values: Collection<T>): Map<T, Int> {
    val countMap = mutableMapOf<T, Int>().withDefault { 0 }
    for (e in values) {
        countMap[e] = countMap.getValue(e) + 1
    }
    return countMap
}

它进一步简化了代码,因为我们不再需要在迭代中处理未知值了。由于我们正在使用默认地图,我们可以安全地从中获得值并随着我们走的递增。

重要的提示

There's one important thing you need to be aware of when using the withDefault extension, which is part of its documentation:

[...]当原始贴图不包含指定密钥的值时使用此隐式默认值,并且值是 用[map.getValue]功能获得 [...]

The default value will only be provided if you use Map::getValue, which is not the case when accessing the map with index operators as you can observe here (hit the run button):

fun main(){
    val map = mutableMapOf<String, Int>().withDefault { 0 }
    println(map["hello"])
    println(map.getValue("hello")) 
}

The reason for this is the contract of the Map interface, which says:

Returns the value corresponding to the given [key], or null if such a key is not present in the map.

Since default maps want to fulfill this contract, they cannot return anything but null in the case of non-existent keys, which has been discussed in the kotlin论坛 before.

让我们去惯用

好吧,所以看到Kotlin确实是默认值的手段,这很棒。但是,正如我们在上面的示例中写得惯用的代码是惯用的?我认为这取决于。使用Kotlin的出色功能API可以解决大多数情况,这与内置分组和计数功能有关。尽管如此,知道替代方案是很好的,因为在某些情况下,您可能会选择采用迭代方法。让我为您提供简化版本的代码:

fun <T> countOccurrences(values: Collection<T>): Map<T, Int> =
    mutableMapOf<T, Int>().withDefault { 0 }.apply {
        for (e in values) {
            put(e, getValue(e) + 1)
        }
    }

It still uses an explicit for loop but got simplified by the use of apply which allows us to initialize the map in a single statement.

有几十种方法可以在Kotlin中解决给定的问题,但可能是最简单的问题 功能方法:

fun <T> countOccurrences(values: Collection<T>): Map<T, Int> =
    values.groupingBy { it }.eachCount()

结论

As shown in this little article, you can solve simple algorithms in many different ways using Kotlin. Similar to Python, you can initialize a map with a default value provider but need to be aware that the regular index operation access won't work the way you might expect. As documented, the getValue function has to be used. Since Kotlin comes with a very useful standard library, you want to consider using it as much as possible. This means that implementing common things like grouping or counting should, in most cases, not be implemented from scratch but rather be done with existing functionalities from the standard library.

One thought on “Kotlin的默认地图

发表评论

您的电子邮件地址不会被公开。 必需的地方已做标记 *