Я хочу "вырваться" из forEach и вернуться непосредственно к вызывающему bar при определенных условиях.
С List это работает:
fun bar(collection: List<String>): Boolean {
collection.forEach { value ->
if ("foo".equals(value))
return true
}
return false
}
Но если collection - это Map, я получаю ошибку компиляции:
fun bar(collection: Map<String, String>): Boolean {
collection.forEach { key, value ->
if ("foo".equals(key))
return true // compilation error: 'return' is not allowed here.
}
return false
}
Почему?
(Пожалуйста, не возражайте против использования forEach для простого поиска в этом случае. Это всего лишь минимальный образец. Фактический код намного сложнее.)
возможно, в этом конкретном случае вам также будет интересно следующее: Справка по Kotlin - преобразования SAM





Map имеет другую реализацию forEach. Вы можете посмотреть исходный код.
Для List:
public inline fun <T> Iterable<T>.forEach(action: (T) -> Unit): Unit {
for (element in this) action(element)
}
Для Map (это java):
default void forEach(BiConsumer<? super K, ? super V> action) {
Objects.requireNonNull(action);
for (Map.Entry<K, V> entry : entrySet()) {
K k;
V v;
try {
k = entry.getKey();
v = entry.getValue();
} catch (IllegalStateException ise) {
// this usually means the entry is no longer in the map.
throw new ConcurrentModificationException(ise);
}
action.accept(k, v);
}
}
list.forEach принимает function type, а map.forEach принимает экземпляр BiConsumer.
Для List, как предлагается ключевым словом inline, вы можете заменить вызов forEach на
for (value in collection)
{
if ("foo".equals(value))
{
return true
}
}
и все имеет смысл с возвратом.
Лямбда, передаваемая в map.forEach, на самом деле является реализацией функции-члена accept интерфейса BiConsumer, тип которой - void. Поэтому возвращать Boolean не имеет смысла. Даже если вы просто return, он просто завершит метод accept. Поскольку это не функция kotlin inline, она не завершает функцию включения.
Исходный код BiConsumer на Java
public interface BiConsumer<T, U> {
/**
* Performs this operation on the given arguments.
*
* @param t the first input argument
* @param u the second input argument
*/
void accept(T t, U u);
/**
* Returns a composed {@code BiConsumer} that performs, in sequence, this
* operation followed by the {@code after} operation. If performing either
* operation throws an exception, it is relayed to the caller of the
* composed operation. If performing this operation throws an exception,
* the {@code after} operation will not be performed.
*
* @param after the operation to perform after this operation
* @return a composed {@code BiConsumer} that performs in sequence this
* operation followed by the {@code after} operation
* @throws NullPointerException if {@code after} is null
*/
default BiConsumer<T, U> andThen(BiConsumer<? super T, ? super U> after) {
Objects.requireNonNull(after);
return (l, r) -> {
accept(l, r);
after.accept(l, r);
};
}
}
Без всяких упрощений ваша функция на самом деле выглядит так:
fun bar(collection: Map<String, String>): Boolean {
val action : BiConsumer<String,String> = object : BiConsumer<String, String> {
override fun accept(t: String, u: String) {
//return boolean is not allow here
//return at here just end the accept function. bar is not affected
}
}
collection.forEach(action)
return false
}
Поскольку kotlin преобразует реализацию интерфейса с одним методом в лямбда, это дает вам иллюзию, что map.forEach выглядит как встроенный вызов, принимающий function type, точно так же, как List. На самом деле лямбда, принятая map.forEach, - это не kotlin function type, а реализация BiConsumer, и, что наиболее важно, это не inline.
Ответ Рики Мо находится на правильном пути в объяснении, почему возникает ваша ошибка, но я думаю, что есть еще кое-что, что можно добавить о том, как ее решить.
Кратко резюмируя этот ответ:
forEach на List вызывает функцию стандартной библиотеки Kotlin Iterable.forEach, которая является встроенной функцией таким образом позволяя вам вернуться из bar внутри передаваемой ей лямбды. Эта функция принимает лямбда-параметр, который сам имеет только один параметр.Map, вы фактически вызываете метод Java forEach, определенный на Map, который принимает BiConsumer, интерфейс, который по сути является двухпараметрическим лямбда-выражением. В Java нет концепции встраивания, поэтому вы не можете выполнить нелокальный возврат из этого BiConsumer.Давайте поговорим о решениях.
Вы также можете использовать Kotlin Iterable.forEach в случае Map, так как это являетсяIterable. Чтобы вызвать этот forEach, вам просто нужно передать лямбда, которая принимает один параметр вместо двух:
collection.forEach { entry ->
if ("foo".equals(entry.key))
return true
}
Здесь возврат будет работать, поскольку этоforEach встроен.
Вы также можете сделать предыдущий вызов таким же образом, используя деструктуризация в записях карты:
collection.forEach { (key, value) ->
if ("foo".equals(key))
return true
}
Этот синтаксис очень близок к вашему исходному вызову (возможно, это раздражает), но эта лямбда по-прежнему имеет один параметр, что делает его вызовом функции forEach стандартной библиотеки Kotlin вместо метода Java, который принимает два параметра.
В качестве последнего незначительного шага вы можете использовать _ в качестве имени значения, если вы не используете его в лямбда-выражении:
collection.forEach { (key, _) ->
if ("foo".equals(key))
return true
}
обратите внимание, что почти все, что может быть выражено с помощью
forEach, также может быть выражено другими средствами ... и часто это приводит к чему-то более простому в конце ... но может потребоваться больше времени, чтобы найти подходящее решение ... (но иногда вы видите, что, возможно, вы выбрали неправильную структуру данных для начала) ... Я просто упоминаю это, потому что вы говорите, что это всего лишь минимальный образец, но оригинал намного сложнее ;-))