Java 8 Iterable.forEach() vs foreach loop

Какое из следующего является лучшей практикой в ​​Java 8?

Java 8:

joins.forEach(join -> mIrc.join(mSession, join));

Java 7:

for (String join : joins) {
    mIrc.join(mSession, join);
}

У меня много циклов, которые могут быть "упрощены" с помощью lambdas, но действительно ли есть какое-то преимущество в их использовании, включая производительность и удобочитаемость?

ИЗМЕНИТЬ

Я также продолжу этот вопрос до более длинных методов - я знаю, что вы не можете возвратить или разбить родительскую функцию из лямбда, и об этом следует упомянуть, если они сравниваются, но есть ли что-то еще, что нужно учитывать?

+330
19 мая '13 в 13:57
источник поделиться
9 ответов

Преимущество заключается в том, что операции могут выполняться параллельно. (См. http://java.dzone.com/articles/devoxx-2012-java-8-lambda-and - раздел о внутренней и внешней итерации)

  • Основное преимущество моей точки зрения заключается в том, что реализация того, что должно быть сделано в цикле, может быть определена без необходимости решать, будет ли она выполняться параллельно или последовательно.

  • Если вы хотите, чтобы ваш цикл выполнялся параллельно, вы могли просто написать

     joins.parallelStream().forEach((join) -> mIrc.join(mSession, join));
    

    Вам нужно будет написать дополнительный код для обработки потоков и т.д.

Примечание: для моего ответа я предполагал присоединиться к реализации интерфейса java.util.Stream. Если join реализует только интерфейс java.util.Iterable, это уже не так.

+145
19 мая '13 в 14:05
источник

Лучшей практикой является использование for-each. Помимо нарушения принципа Keep It Simple, Stupid, в новомодном forEach() есть, по крайней мере, следующие недостатки:

  • Нельзя использовать не конечные переменные. Таким образом, код, подобный приведенному ниже, не может быть превращен в forIach лямбда:

    Object prev = null;
    for(Object curr : list)
    {
        if( prev != null )
            foo(prev, curr);
        prev = curr;
    }
    
  • Невозможно обработать проверенные исключения. На Lambdas фактически запрещено запрещать проверенные исключения, но общие функциональные интерфейсы, такие как Consumer, не объявляют. Поэтому любой код, который выдает проверенные исключения, должен обернуть их в try-catch или Throwables.propagate(). Но даже если вы это сделаете, не всегда понятно, что происходит с выброшенным исключением. Его можно было усвоить где-то в кишках forEach()

  • Ограниченный контроль потока. A return в лямбда равна a continue в для каждого, но нет эквивалента a break. Также трудно делать такие вещи, как возвращаемые значения, короткое замыкание или установить флаги (что немного облегчило бы ситуацию, если бы это не было нарушением правила не конечных переменных). "Это не просто оптимизация, но и критическая, когда вы считаете, что некоторые последовательности (например, чтение строк в файле) могут иметь побочные эффекты, или вы можете иметь бесконечная последовательность" .

  • Может выполняться параллельно, что является ужасным, ужасным для всех, кроме 0,1% вашего кода, который необходимо оптимизировать. Любой параллельный код необходимо продумать (даже если он не использует блокировки, летучие компоненты и другие особенно неприятные аспекты традиционного многопоточного исполнения). Любая ошибка будет трудно найти.

  • Может ухудшить производительность, поскольку JIT не может оптимизировать forEach() + lambda в той же степени, что и обычные циклы, особенно теперь, когда lambdas являются новыми. Под "оптимизацией" я не имею в виду накладные расходы на вызов lambdas (что мало), но на сложный анализ и трансформацию, которые выполняет современный JIT-компилятор при запуске кода.

  • Если вам действительно нужно parallelism, вероятно, гораздо быстрее и не сложнее использовать ExecutorService. Потоки являются как автоматическими (читайте: не много разбирайтесь в своей проблеме), и используйте специализированную стратегию распараллеливания (читайте: неэффективно для общего случая) (рекурсивное разложение fork-join).

  • Делает отладку более запутанной из-за иерархии вложенных вызовов и, бог, запрещает параллельное выполнение. Отладчик может иметь проблемы с отображением переменных из окружающего кода, и такие вещи, как пошаговое выполнение, могут работать не так, как ожидалось.

  • Потоки в целом сложнее кодировать, читать и отлаживать. На самом деле это относится к сложным API fluent "в целом. Комбинация сложных одиночных утверждений, интенсивное использование дженериков и отсутствие промежуточных переменных заставляют создавать путаные сообщения об ошибках и отлаживать отладку. Вместо того, чтобы" этот метод не имеет перегрузки для типа X ", вы получаете сообщение об ошибке ближе к" где-то вы испортили типы, но мы не знаем, где и как". Точно так же вы не можете проходить и анализировать вещи в отладчике так же легко, как когда код разбит на несколько операторов, а промежуточные значения сохраняются в переменных. Наконец, чтение кода и понимание типов и поведения на каждом этапе выполнения могут быть нетривиальными.

  • Выкладывается как больной палец. Язык Java уже имеет оператор for-each. Зачем заменить его вызовом функции? Зачем поощрять скрывать побочные эффекты где-то в выражениях? Зачем поощрять громоздкие однострочники? Смешение регулярных для каждого и нового forEach волей-неволей - плохой стиль. Кодекс должен говорить в идиомах (шаблоны, которые быстро осмысливаются из-за их повторения), и чем меньше идиом используется, тем понятнее код и меньше времени тратится на решение, какую идиому использовать (большой тайм-стоп для таких перфекционистов, как я!).

Как вы можете видеть, я не большой поклонник forEach(), за исключением случаев, когда это имеет смысл.

Особенно оскорбительным для меня является тот факт, что Stream не реализует Iterable (несмотря на то, что на самом деле имеет метод iterator) и не может использоваться в for-each, только с forEach(). Я рекомендую передавать потоки в Iterables с помощью (Iterable<T>)stream::iterator. Лучшей альтернативой является использование StreamExкоторый исправляет ряд проблем Stream API, включая реализацию Iterable.

Тем не менее, forEach() полезно для следующего:

  • Атомная итерация по синхронизированному списку. До этого список, сгенерированный с помощью Collections.synchronizedList(), был атомарным по отношению к вещам типа get или set, но не был потокобезопасным при итерации.

  • Параллельное выполнение (с использованием соответствующего параллельного потока). Это избавит вас от нескольких строк кода и использования ExecutorService, если ваша проблема соответствует предположениям производительности, встроенным в потоки и разделители.

  • Конкретные контейнеры, которые, как и синхронизированный список, получают выгоду от контроля за итерацией (хотя это в значительной степени теоретическое, если люди не могут привести больше примеров)

  • Вызов одной функции более чисто с помощью forEach() и аргумента ссылки метода (т.е. list.forEach (obj::someMethod)). Однако имейте в виду пункты проверенных исключений, более сложную отладку и уменьшайте количество идиом, которые вы используете при написании кода.

Статьи, которые я использовал для ссылки:

EDIT: Похоже, что некоторые из оригинальных предложений для лямбда (например, http://www.javac.info/closures-v06a.html) решили некоторые из проблемы, о которых я упомянул (при этом, конечно, добавляя свои собственные сложности).

+343
24 нояб. '13 в 16:45
источник

Связанные вопросы


Похожие вопросы

При чтении этого вопроса можно получить впечатление, что Iterable#forEach в сочетании с лямбда-выражениями является ярлыком/заменой для записи традиционного для каждого цикла. Это просто неправда. Этот код из OP:

joins.forEach(join -> mIrc.join(mSession, join));

не, предназначенный как ярлык для записи

for (String join : joins) {
    mIrc.join(mSession, join);
}

и, безусловно, не следует использовать таким образом. Вместо этого он предназначен как ярлык (хотя это не совсем то же самое) для записи

joins.forEach(new Consumer<T>() {
    @Override
    public void accept(T join) {
        mIrc.join(mSession, join);
    }
});

И это заменяет следующий код Java 7:

final Consumer<T> c = new Consumer<T>() {
    @Override
    public void accept(T join) {
        mIrc.join(mSession, join);
    }
};
for (T t : joins) {
    c.accept(t);
}

Замена тела цикла функциональным интерфейсом, как в приведенных выше примерах, делает ваш код более явным: вы говорите, что (1) тело цикла не влияет на окружающий код и поток управления и ( 2) тело цикла может быть заменено другой реализацией функции, не затрагивая окружающий код. Невозможность доступа к не конечным переменным внешней области не является недостатком функций /lambdas, это функция, которая отличает семантику Iterable#forEach от семантики традиционного для каждого цикла, Как только вы привыкнете к синтаксису Iterable#forEach, он делает код более читаемым, потому что вы сразу получаете эту дополнительную информацию о коде.

Традиционные для каждого цикла, безусловно, останутся хорошей практикой (чтобы избежать злоупотребления термином "лучшая практика" ) на Java. Но это не означает, что Iterable#forEach следует рассматривать как плохую практику или плохой стиль. Всегда полезно использовать правильный инструмент для выполнения задания, и это включает в себя смешивание традиционных для каждого цикла с Iterable#forEach, где это имеет смысл.

Поскольку недостатки Iterable#forEach уже обсуждались в этом потоке, вот несколько причин, почему вы, возможно, захотите использовать Iterable#forEach:

  • Чтобы сделать ваш код более явным: Как описано выше, Iterable#forEach может сделать ваш код более явным и читаемым в некоторых ситуациях.

  • Чтобы сделать ваш код более расширяемым и поддерживаемым: Использование функции в качестве тела цикла позволяет заменить эту функцию различными реализациями (см. Шаблон стратегии). Вы можете, например, легко заменить выражение лямбда вызовом метода, который может быть перезаписан подклассами:

    joins.forEach(getJoinStrategy());
    

    Затем вы можете предоставить стратегии по умолчанию, используя перечисление, которое реализует функциональный интерфейс. Это не только делает ваш код более расширяемым, но и повышает ремонтопригодность, поскольку он отделяет реализацию цикла от объявления цикла.

  • Чтобы сделать ваш код более отлажимым: Отключение реализации цикла из декларации также облегчает отладку, поскольку вы можете иметь специализированную реализацию отладки, которая выводит отладочные сообщения без необходимость загромождать ваш основной код с помощью if(DEBUG)System.out.println(). Реализация отладки может, например, быть делегатом, что украшает фактическую реализацию функции.

  • Чтобы оптимизировать критически важный для производительности код: В отличие от некоторых утверждений в этом потоке, Iterable#forEach уже обеспечивает лучшую производительность, чем традиционный для каждого цикла, по крайней мере, при использовании ArrayList и запустить Hotspot в режиме "-client". Хотя это увеличение производительности мало и незначительно для большинства случаев использования, бывают ситуации, когда эта дополнительная производительность может иметь значение. Например. разработчики библиотек обязательно захотят оценить, если некоторые из существующих реализаций цикла должны быть заменены на Iterable#forEach.

    Чтобы подтвердить это утверждение фактами, я сделал несколько микро-тестов с Caliper. Вот тестовый код (требуется последний калибр от git):

    @VmOptions("-server")
    public class Java8IterationBenchmarks {
    
        public static class TestObject {
            public int result;
        }
    
        public @Param({"100", "10000"}) int elementCount;
    
        ArrayList<TestObject> list;
        TestObject[] array;
    
        @BeforeExperiment
        public void setup(){
            list = new ArrayList<>(elementCount);
            for (int i = 0; i < elementCount; i++) {
                list.add(new TestObject());
            }
            array = list.toArray(new TestObject[list.size()]);
        }
    
        @Benchmark
        public void timeTraditionalForEach(int reps){
            for (int i = 0; i < reps; i++) {
                for (TestObject t : list) {
                    t.result++;
                }
            }
            return;
        }
    
        @Benchmark
        public void timeForEachAnonymousClass(int reps){
            for (int i = 0; i < reps; i++) {
                list.forEach(new Consumer<TestObject>() {
                    @Override
                    public void accept(TestObject t) {
                        t.result++;
                    }
                });
            }
            return;
        }
    
        @Benchmark
        public void timeForEachLambda(int reps){
            for (int i = 0; i < reps; i++) {
                list.forEach(t -> t.result++);
            }
            return;
        }
    
        @Benchmark
        public void timeForEachOverArray(int reps){
            for (int i = 0; i < reps; i++) {
                for (TestObject t : array) {
                    t.result++;
                }
            }
        }
    }
    

    И вот результаты:

    При работе с "-client", Iterable#forEach превосходит традиционный цикл for через ArrayList, но все еще медленнее, чем прямое повторение массива. При работе с "-сервером" производительность всех подходов примерно одинакова.

  • Предоставить дополнительную поддержку для параллельного выполнения:. Здесь уже сказано, что возможность параллельного выполнения функционального интерфейса Iterable#forEach с использованием потоки, безусловно, является важным аспектом. Поскольку Collection#parallelStream() не гарантирует, что цикл фактически выполняется параллельно, необходимо учитывать это необязательную функцию. Итерируя по вашему списку с помощью list.parallelStream().forEach(...);, вы явно говорите: этот цикл поддерживает параллельное выполнение, но от него не зависит. Опять же, это особенность, а не дефицит!

    Перемещая решение для параллельного выполнения в сторону от вашей фактической реализации цикла, вы разрешаете необязательную оптимизацию своего кода, не затрагивая сам код, что хорошо. Кроме того, если реализация параллельного потока по умолчанию не соответствует вашим потребностям, никто не мешает вам выполнять вашу собственную реализацию. Вы можете, например, обеспечить оптимизированную коллекцию в зависимости от базовой операционной системы, размера коллекции, количества ядер и некоторых настроек предпочтений:

    public abstract class MyOptimizedCollection<E> implements Collection<E>{
        private enum OperatingSystem{
            LINUX, WINDOWS, ANDROID
        }
        private OperatingSystem operatingSystem = OperatingSystem.WINDOWS;
        private int numberOfCores = Runtime.getRuntime().availableProcessors();
        private Collection<E> delegate;
    
        @Override
        public Stream<E> parallelStream() {
            if (!System.getProperty("parallelSupport").equals("true")) {
                return this.delegate.stream();
            }
            switch (operatingSystem) {
                case WINDOWS:
                    if (numberOfCores > 3 && delegate.size() > 10000) {
                        return this.delegate.parallelStream();
                    }else{
                        return this.delegate.stream();
                    }
                case LINUX:
                    return SomeVerySpecialStreamImplementation.stream(this.delegate.spliterator());
                case ANDROID:
                default:
                    return this.delegate.stream();
            }
        }
    }
    

    Приятно, что ваша реализация цикла не должна знать или заботиться об этих деталях.

+84
19 мар. '14 в 10:02
источник

forEach() может быть реализована быстрее, чем для каждого цикла, потому что итеративный знает лучший способ повторить его элементы, в отличие от стандартного итератора. Таким образом, разница - это внутренняя или внутренняя петля.

Например, ArrayList.forEach(action) может быть просто реализован как

for(int i=0; i<size; i++)
    action.accept(elements[i])

в отличие от цикла for-each, для которого требуется много лесов

Iterator iter = list.iterator();
while(iter.hasNext())
    Object next = iter.next();
    do something with `next`

Однако нам также нужно учитывать две накладные расходы, используя forEach(), один из которых делает лямбда-объект, а другой вызывает метод лямбда. Они, вероятно, не имеют значения.

см. также http://journal.stuffwithstuff.com/2013/01/13/iteration-inside-and-out/ для сравнения внутренних/внешних итераций для разных случаев использования.

+6
19 мая '13 в 18:00
источник

TL; DR: List.stream().forEach() был самым быстрым.

Я чувствовал, что должен добавить свои результаты из сравнения итераций. Я сделал очень простой подход (без базовых тестов) и сравнил 5 различных методов:

  • classic for
  • классический foreach
  • List.forEach()
  • List.stream().forEach()
  • List.parallelStream().forEach

процедура и параметры тестирования

private List<Integer> list;
private final int size = 1_000_000;

public MyClass(){
    list = new ArrayList<>();
    Random rand = new Random();
    for (int i = 0; i < size; ++i) {
        list.add(rand.nextInt(size * 50));
    }    
}
private void doIt(Integer i) {
    i *= 2; //so it won't get JITed out
}

Список этого класса должен быть повторен и иметь некоторые doIt(Integer i), применяемые ко всем его членам, каждый раз с помощью другого метода. в основном классе я запускаю проверенный метод три раза, чтобы разогреть JVM. Затем я запускаю метод проверки 1000 раз, суммируя время, необходимое для каждого итерационного метода (используя System.nanoTime()). После этого я делю эту сумму на 1000 и что результат - среднее время. Пример:

myClass.fored();
myClass.fored();
myClass.fored();
for (int i = 0; i < reps; ++i) {
    begin = System.nanoTime();
    myClass.fored();
    end = System.nanoTime();
    nanoSum += end - begin;
}
System.out.println(nanoSum / reps);

Я запускал это на i5 4-ядерном процессоре, с java-версией 1.8.0_05

classic for

for(int i = 0, l = list.size(); i < l; ++i) {
    doIt(list.get(i));
}

время выполнения: 4.21 мс

классический foreach

for(Integer i : list) {
    doIt(i);
}

время выполнения: 5.95 мс

List.forEach()

list.forEach((i) -> doIt(i));

время выполнения: 3.11 мс

List.stream().forEach()

list.stream().forEach((i) -> doIt(i));

время выполнения: 2,79 мс

List.parallelStream().forEach

list.parallelStream().forEach((i) -> doIt(i));

время выполнения: 3,6 мс

+5
15 сент. '14 в 19:39
источник

Одним из наиболее ограничивающих функциональных ограничений forEach является отсутствие поддержки проверенных исключений.

Один возможный обходной путь заключается в замене терминала forEach на простой старый цикл foreach:

    Stream<String> stream = Stream.of("", "1", "2", "3").filter(s -> !s.isEmpty());
    Iterable<String> iterable = stream::iterator;
    for (String s : iterable) {
        fileWriter.append(s);
    }

Вот список наиболее популярных вопросов с другими обходными решениями по проверке обработки исключений в lambdas и потоках:

Функция Java 8 Lambda, которая выдает исключение?

Java 8: Лямбда-потоки, Фильтр по методу с исключением

Как я могу удалить CHECKED исключения изнутри потоков Java 8?

Java 8: Обязательная проверка обработки исключений в лямбда-выражениях. Почему обязательный, а не факультативный?

+3
29 июл. '15 в 17:55
источник

Я чувствую, что мне нужно немного расширить свой комментарий...

О парадигме\стиле

Это, наверное, самый заметный аспект. FP стал популярным благодаря тому, что вы можете избежать побочных эффектов. Я не буду углубляться в то, что профи\минусы вы можете получить от этого, так как это не связано с вопросом.

Тем не менее, я скажу, что итерация с использованием Iterable.forEach вдохновлена ​​FP и, скорее, результатом приведения большего количества FP в Java (по иронии судьбы, я бы сказал, что для чистого EE в чистом FP ​​нет большого смысла, поскольку он ничего, кроме введения побочных эффектов).

В конце я бы сказал, что это скорее вопрос вкуса \style\paradigm, который вы сейчас пишете.

О parallelism.

С точки зрения производительности нет обещанных заметных преимуществ от использования Iterable.forEach over foreach (...).

Согласно официальному docs на Iterable.forEach:

Выполняет данное действие над содержимым Iterable, в элементы порядка появляются при повторении, пока все элементы не будут обрабатывается или действие выдает исключение.

... т.е. документы довольно ясно, что не будет подразумеваемых parallelism. Добавление одного из них будет нарушением LSP.

Теперь есть "параллельные коллекции", которые обещаны в Java 8, но для работы с теми, которые вам нужны для меня более явными, и уделять особое внимание их использованию (см. например, ответ mschenk74).

BTW: в этом случае Stream.forEach будет использоваться, и это не гарантирует, что фактическая работа будет выполнена в параллельном режиме (зависит на базовой коллекции).

ОБНОВЛЕНИЕ: может быть не так очевидно и немного растянуто с первого взгляда, но есть еще один аспект перспективы стиля и читаемости.

Прежде всего - простые старые ловушки простые и старые. Все уже знают их.

Во-вторых, и что более важно - вы, вероятно, захотите использовать Iterable.forEach только с однострочным lambdas. Если "тело" становится тяжелее - они, как правило, не являются такими читаемыми. У вас есть 2 варианта отсюда - используйте внутренние классы (yuck) или используйте обычный старый forloop. Люди часто раздражаются, когда видят одни и те же вещи (итератины над коллекциями), делаются различные vays/styles в одной и той же кодовой базе, и это похоже на то, что происходит.

Опять же, это может быть или не быть проблемой. Зависит от людей, работающих с кодом.

+3
19 мая '13 в 16:31
источник

Преимущество Java 1.8 forEach по сравнению с 1.7 Enhanced for loop заключается в том, что при написании кода вы можете сосредоточиться только на бизнес-логике.

forEach метод принимает java.util.function.Consumer объект как аргумент, поэтому Он помогает в том, чтобы наша бизнес-логика находилась в отдельном месте, которое вы можете повторно использовать в любое время.

Посмотрите ниже фрагмент,

  • Здесь я создал новый класс, который будет переопределять метод класса accept из класса Consumer,       где вы можете добавить дополнительную функциональность, Больше, чем Итерация..!!!!!!

    class MyConsumer implements Consumer<Integer>{
    
        @Override
        public void accept(Integer o) {
            System.out.println("Here you can also add your business logic that will work with Iteration and you can reuse it."+o);
        }
    }
    
    public class ForEachConsumer {
    
        public static void main(String[] args) {
    
            // Creating simple ArrayList.
            ArrayList<Integer> aList = new ArrayList<>();
            for(int i=1;i<=10;i++) aList.add(i);
    
            //Calling forEach with customized Iterator.
            MyConsumer consumer = new MyConsumer();
            aList.forEach(consumer);
    
    
            // Using Lambda Expression for Consumer. (Functional Interface) 
            Consumer<Integer> lambda = (Integer o) ->{
                System.out.println("Using Lambda Expression to iterate and do something else(BI).. "+o);
            };
            aList.forEach(lambda);
    
            // Using Anonymous Inner Class.
            aList.forEach(new Consumer<Integer>(){
                @Override
                public void accept(Integer o) {
                    System.out.println("Calling with Anonymous Inner Class "+o);
                }
            });
        }
    }
    
+2
23 дек. '17 в 14:59
источник

У меня есть тест с кодом, как показано ниже:

public class TestPerformance {

    public static void main(String[] args) {
        List<Integer> numbers = getNumbers();
        testJava7(numbers);
        testForeachJava7(numbers); 
        testJava8(numbers);
        testStreamJava8(numbers);
        testParallelStreamJava8(numbers);
    }

    private static void testJava7(List<Integer> numbers){
        long startTime = System.currentTimeMillis();
        int size = numbers.size();
        for(int i = 0; i < size; i++){
            numbers.get(i);
        }
        long endTime = System.currentTimeMillis();
        long totalTime = endTime - startTime;
        System.out.println("testJava7 " + totalTime + " ms");
    }

    private static void testForeachJava7(List<Integer> numbers){
        long startTime = System.currentTimeMillis();
        for(Integer num : numbers){

        }
        long endTime = System.currentTimeMillis();
        long totalTime = endTime - startTime;
        System.out.println("testForeachJava7 " + totalTime + " ms");
    }

    private static void testJava8(List<Integer> numbers){
        long startTime = System.currentTimeMillis();
        numbers.forEach(s -> {});
        long endTime = System.currentTimeMillis();
        long totalTime = endTime - startTime;
        System.out.println("testJava8 " + totalTime + " ms");
    }

    private static void testStreamJava8(List<Integer> numbers){
        long startTime = System.currentTimeMillis();
        numbers.stream().forEach(s -> {});
        long endTime = System.currentTimeMillis();
        long totalTime = endTime - startTime;
        System.out.println("testStreamJava8 " + totalTime + " ms");
    }

    private static void testParallelStreamJava8(List<Integer> numbers){
        long startTime = System.currentTimeMillis();
        numbers.parallelStream().forEach(s -> {});
        long endTime = System.currentTimeMillis();
        long totalTime = endTime - startTime;
        System.out.println("testParallelStreamJava8 " + totalTime + " ms");
    }

    private static List<Integer> getNumbers(){
        List<Integer> numbers = new ArrayList<>();
        for (int i = 1; i<=5000000; i++){
            numbers.add(i);
        }
        return numbers;
    }

}

Результат:

testJava7 4 ms
testForeachJava7 17 ms
testJava8 65 ms
testStreamJava8 17 ms
testParallelStreamJava8 27 ms

В результате выше: лучшая производительность для (int i; я < size; я ++) → рядом с foreach (item: list) и list.stream.foreach(s → {}) → рядом с list.parallelStream.foreach(s → {}) → last - list.foreach(s → {})

-1
05 окт. '17 в 14:06
источник

Посмотрите другие вопросы по меткам или Задайте вопрос