Как стать автором
Поиск
Написать публикацию
Обновить

Комментарии 44

Например, сохраняет ли "фильтр четных чисел" четные числа или отбрасывает их?

Ну это легкотня. Воздушный фильтр пропускает воздух, и задерживает вас остальное.

А предикат чётных чисел что делает? :)

Захват переменной - понятен, замыкание - не понятно.

Вечная беда замыканий - неявный захват контекста и как результат утекание его. Это частично решено умными замыканиями где контекст не захватывается если нет обращений к нему, но это создаёт ложное ощущение безопастности. А если уж разворачиваться в полный рост, с карированием - там потом черт ногу сломает искать куда тебя утёк контекст. Так что инструмент отличный и красивый, но острый и опасный.

ложное ощущение безопастности

Начиная с C# 9 можно объявлять статические замыкания, это уже придаёт некое ощущение безопасности.

Если говорить простым языком, то замыкания (closures)...

Проще не скажешь!

как все это работает под капотом

У "под капотом" есть интересный побочный эффект - то, что нельзя замыкать переменные типа ref struct (в частности, Span<T>), и то, что замыкание "value type" переменной приводит к её boxing/unboxing.

нет там никакого боксинга.

Есть. Именно поэтому Span<T> замкнуть и не получается - замкнутые value-type переменные "уезжают" в кучу (heap).

Жаль нельзя на большие деньги поспорить, погулял бы на НГ за чужой счёт в кои-то веки.

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

"Уезжать в кучу" != боксинг.

"Уезжать в кучу" != боксинг.

Офигеть... А что же по-вашему такое тогда боксинг? :-О

Здесь при вызове GetMultiplyBy(42) будет боксинг?

public Func<int> GetMultiplyBy(int x) =>  y => y *x;

А что же по-вашему такое тогда боксинг? :-О

А вот это можно почитать и в учебнике.

Конкретно в C# боксингом называется использование операции box. В вашем коде её нет.

В более общем смысле, боксинг - это создание индивидуального объекта-обёртки ("коробки") для значения. Формально ваша GetMultiplyBy действительно создаёт такую коробку, но это происходит вовсе не потому что int является value типом. К примеру, вот такая функция так же создаст скоуп, попадающий под определение "коробки":

Func<string, strting> GetConcatenatedBy(string x) => y => y + x;

Конкретно в C# боксингом называется использование операции box.

Нет.

Вот, можете ознакомиться, если интересно. Там описаны случаи, когда boxing есть, но явная инструкция box в IL отсутствует.

Отлично, первые шаги к изучению учебника сделаны.

Вот, можете ознакомиться,

А можно теперь конкретную цитату, которая относится к обсуждаемому случаю?

boxing есть, но явная инструкция box в IL отсутствует.

Любопытно было бы увидеть примеры, не поделитесь?

Отлично, первые шаги к изучению учебника сделаны.

Давайте договоримся только без личностей, хорошо? Я на .NET, не слезая, с момента его появления - совершенно не знаю ваш возраст, но, возможно, вы тогда еще вообще читать не умели :)

Любопытно было бы увидеть примеры, не поделитесь?

Foo foo = new();
Console.WriteLine(foo.ToString());

internal struct Foo
{
}
    // [1 1 - 1 17]
    IL_0000: ldloca.s     foo
    IL_0002: initobj      Foo

    // [2 1 - 2 35]
    IL_0008: ldloca.s     foo
    IL_000a: constrained. Foo
    IL_0010: callvirt     instance string [System.Runtime]System.Object::ToString()
    IL_0015: call         void [System.Console]System.Console::WriteLine(string)
    IL_001a: nop
    IL_001b: ret

Boxing возникает неявно на инструкции IL_0010 и, чтобы подобного избежать надо явно перегружать метод ToString() в структуре Foo

только без личностей, хорошо? ... не знаю ваш возраст

Ноль вопросов, никаких личностей, просто я (лично) считаю, что учиться никогда не поздно и не зазорно, а иногда ещё и нужно ;)

Boxing возникает неявно на инструкции IL_0010

Вообще, не сказал бы, что прям неявно:

https://learn.microsoft.com/en-us/dotnet/api/system.reflection.emit.opcodes.constrained?view=net-8.0&redirectedfrom=MSDN

When a callvirt method instruction has been prefixed by constrained thisType, the instruction is executed as follows:

  • If thisType is a reference type (as opposed to a value type) then ptr is dereferenced and passed as the 'this' pointer to the callvirt of method.

  • If thisType is a value type and thisType implements method then ptr is passed unmodified as the 'this' pointer to a call method instruction, for the implementation of method by thisType.

  • If thisType is a value type and thisType does not implement method then ptr is dereferenced, boxed, and passed as the 'this' pointer to the callvirt method instruction.

This last case can occur only when method was defined on ObjectValueType, or Enum and not overridden by thisType. In this case, the boxing causes a copy of the original object to be made. However, because none of the methods of ObjectValueType, and Enum modify the state of the object, this fact cannot be detected.

Но ок, box в IL действительно нет, спасибо за напоминание.

Тем не менее, что насчёт обсуждаемого случая?

Есть там цитата?

Тем не менее, что насчёт обсуждаемого случая?

Ну, тут я говорил насчет того, что:

Конкретно в C# боксингом называется использование операции box.

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

Про замыкания, ну бокс бог с ним - не будем называть это прямо "боксингом"

Не "не будем", а "нельзя".

Боксинг - хоть в .НЕТ, хоть в абстрактном CS-определении - это не "копировние из стека в кучу" и кроме аллокации тянет за собой и вычислительные расходы.

В случае с захватом этого не происходит.

Более того, и самой локальной переменной на стеке вообще [в большинстве случаев] не окажется, она сразу будет определена в классе, созданном компилятором для лямбды.

но факт, что (возможно, нежелательное) копирование из стека в кучу при замыкании вполне возможно

Не "вполне возможно", а обязательно произойдёт, бай дизайн, так сказать.

"Уезжают" в кучу не просто value-type переменные, а вообще все замкнутые переменные.

Именно потому боксингом это и не является.

Вы можете называть замыкания в Java и C# красивыми лишь до тех пор, пока не познакомитесь с замыканиями на Kotlin. Потому что Kotlin это одно сплошное замыкание.

Лично я в приведённом коде увидел слишком много лишнего, не относящегося к сути, что у неподготовленного читателя вызовет много вопросов.

Ну что в котлине действительно хорошо, так это it
А в остальном разве что сахар для инлайн классов java хорош, а в остальном то что такого?

Последние шарпы тоже весьма хороши.

На самом деле мне очень нравится ограничение final для замыканий в java
Это явно определяет контекст и ограничения, в отличие от классической проблемы цикла в c#

С "проблемой цикла" можно научиться жить, а вот невозможность изменять захваченную переменную - мне бы мешало.

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

Всё может быть чревато :)

Ну речь о том, чтобы уменьшить количество ситуаций, которые могут привести к проблемам.

Можно ведь отследить когда проблемы бывают чаще - при написании статичных методов, методов с использованием полей или лямбд

Если лямбда простая - отлично, она увеличит локальность и удобство чтения

Но если она захватывает широкий контекст и образует сложную последовательность из изменений переменных - может и лямбду не стоит использовать, но создать объект?

Тут я бы сказал, что если мы используем замыкание как константу в лямбде - это одно, это простая и допустимая логика.

Но если это аккумулятор к примеру - возможно не стоит использовать тут лямбду вообще

Возможно не стоит, а возможно и стоит - написание руками класса с полем и методом, повторяющим то, что сделал бы компилятор для лямбды - это разумно/упрощает/стоит того?

Я склоняюсь к тому, что универсального ответат нет, зато выбор есть.

...но это, к примеру, легко доступная элементарная оптимизация:

Объявляем локальную переменну, объявляем лямбду, которая её захватывает.

В цикле n раз изменяем значение переменной и вызываем метод, в который передаём лямбду - итого одна аллокация вместо n

Ну это даётся ценой неясности

Локальная переменная теперь оказывается не работает как локальная, но работает как метод.

В этом случае можно использовать поле или вообще отдельный объект с полем - памяти уйдет столько же.

Локальная переменная теперь оказывается не работает как локальная, но работает как метод.

А когда локальная переменная передаётся как ref - она перестаёт работать как локальная?

Для рантайма она больше не локальная переменная, но это скрыто и незаметно (пока не упираемся в особые случаи типа ref [struct]). Ну, чтож.

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

А в каких кейсах может понадобиться мокать лямбду?

При тестировании фильтра? (Стратегии в широком смысле)

А есть примеры кода? Что то пока непонятно.

Я вот как-то неудачно выразился - ночь поздняя была плюс алкоголь. Наверное, нужно было написать: "На лямбды тяжело писать какие-либо assert-ы".

Вот, например
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;

var people = new[] {
    new Person(1, "Foo", 42),
    new Person(2, "Bar", 69),
    new Person(2, "Baz", 17),
};

var filtered = people.AsQueryable().Where(
    PersonFilterBuilder.And(
        PersonFilterBuilder.FilterByName("ba"),
        PersonFilterBuilder.FilterByAge(60, 70)));

foreach (var p in filtered)
{
    Console.WriteLine(p);
}

return;

public record Person(int Id, string Name, int Age);

public static class PersonFilterBuilder
{
    public static Expression<Func<Person, bool>> FilterByName(string name) =>
       p => p.Name.StartsWith(name, StringComparison.CurrentCultureIgnoreCase);

    public static Expression<Func<Person, bool>> FilterByAge(int from, int to) =>
       p => from <= p.Age && p.Age <= to;

    public static Expression<Func<Person, bool>> And(
        Expression<Func<Person, bool>> left,
        Expression<Func<Person, bool>> right)
    {
        var p = Expression.Parameter(typeof(Person));

        return Expression.Lambda<Func<Person, bool>>(
            Expression.And(
                new Replace(left.Parameters[0], p).Visit(left.Body),
                new Replace(right.Parameters[0], p).Visit(right.Body)),
            p);
    }
}

public class Replace(Expression from, Expression to) : ExpressionVisitor
{
    [return: NotNullIfNotNull("node")]
    public override Expression? Visit(Expression? node) =>
        node == from ? to : base.Visit(node);
}

Как мне, вот, написать действительно "хороший" тест на метод FilterBuilder.And(...)? Хочу именно тест, который подтвердит мне, что этот метод действительно комбинирует два произвольных выражения через оператор and.

В случае предикатов это очень легко
public record Person(int Id, string Name, int Age);

public static class PersonFilterBuilder
{
    public static Func<Person, bool> FilterByName(string name) =>
       p => p.Name.StartsWith(name, StringComparison.CurrentCultureIgnoreCase);

    public static Func<Person, bool> FilterByAge(int from, int to) =>
       p => from <= p.Age && p.Age <= to;

    public static Func<Person, bool> And(
        Func<Person, bool> left,
        Func<Person, bool> right) =>
        p => left(p) && right(p);
}

public class PersonFilterBuilderTests
{
    [Theory]
    [InlineData(false, false, false)]
    [InlineData(true, false, false)]
    [InlineData(false, true, false)]
    [InlineData(true, true, true)]
    public void And_combines_left_and_right(bool lval, bool rval, bool expected)
    {
        var left = A.Fake<Func<Person, bool>>();
        var right = A.Fake<Func<Person, bool>>();

        Person person = new(1, "blablabla", 666);

        A.CallTo(() => left(person)).Returns(lval);
        A.CallTo(() => right(person)).Returns(rval);

        PersonFilterBuilder.And(left, right)(person).Should().Be(expected);
    }
}

но в случае лямбд все очень сильно усложняется.

Да точно так же для деревьев выражений тест пишется если нужно. Хотя я бы нормализовал выражение и проверил результат ToString()

Кстати, зачем вы вообще мокаете делегаты здесь? Оно же только увеличивает число строк в тесте и запутывает его.

Func<Person, bool> left = _ => lval;
Func<Person, bool> right = _ => rval;

Хотя я бы нормализовал выражение и проверил результат ToString()

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

Кстати, зачем вы вообще мокаете делегаты здесь?

Ну, можно, наверное, и как у вас, но, mock он как бы еще и проверит что делегат left вернет lval именно на вызов с аргументом person (и, соответственно для right) - как-то при этом меньше представляется шансов получить "false positive" (хотя, разумеется, написать тест действительно защищенный от FP на 100% - по-моему это просто в принципе невозможно).

False positive
    // в коде откровенная ересь и тест с mocks на таком коде не пройдет 
    // (последний test case даст false вместо true), а в тесте без mocks все будет ок
    public static Func<Person, bool> And(
        Func<Person, bool> left,
        Func<Person, bool> right) =>
        p => left(null!) && right(null!);
}

Кстати, вот, еще - LINQ-выражения это удобная штука, но для тестирования тоже подарок еще тот. потому что если оно не совсем уж тривиальное, то это по сути конструкция с приличным "cyclomatic complexity"- а такие штуки всегда покрываются тестами очень тяжко и с какой-то матерью.

Поддерживаю вопрос товарища @dopusteam

У меня есть несколько догадок касательно того, что имелось ввиду, но ни в одной не уверен.

НЛО прилетело и опубликовало эту надпись здесь
Зарегистрируйтесь на Хабре, чтобы оставить комментарий