Могут все 'для' циклов быть замененными оператором LINQ?

Действительно ли возможно записать следующий 'foreach' как оператор LINQ, и я предполагаю, что более общий вопрос может кто-либо для цикла быть замененным оператором LINQ.

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

    private static string SomeMethod()
    {
        if (ListOfResources .Count == 0)
            return string.Empty;

        var sb = new StringBuilder();
        foreach (var resource in ListOfResources )
        {
            if (sb.Length != 0)
                sb.Append(", ");

            sb.Append(resource.Id);
        }

        return sb.ToString();
    }

Аплодисменты

AWC

19
задан AwkwardCoder 8 February 2010 в 18:48
поделиться

7 ответов

Конечно. Черт возьми, вы можете заменить арифметику на запросы LINQ:

http://blogs.msdn.com/ericlippert/archive/2009/12/07/query-transformations-are-syntactic.aspx

Но вы не должны.

Назначение выражения запроса состоит в том, чтобы представить операцию запроса . Назначение цикла "for" - выполнить итерацию по определенному выражению, чтобы побочные эффекты выполнялись несколько раз. Часто они сильно отличаются друг от друга. Я рекомендую заменять циклы, целью которых является только запрос данных, на более высокоуровневые конструкции, которые более четко опрашивают данные. Настоятельно не рекомендую заменять код, генерирующий побочные эффекты, на понимание запросов, хотя это возможно.

26
ответ дан 30 November 2019 в 02:36
поделиться

Фактически, ваш код делает что-то, что принципиально очень функционально, а именно сокращает список строк до одной строки, объединяя элементы списка . Единственное, что необходимо в коде, - это использование StringBuilder .

Функциональный код значительно упрощает это, поскольку не требует особого случая, как ваш код. Более того, .NET уже реализовал эту конкретную операцию и, вероятно, более эффективен, чем ваш код 1) :

return String.Join(", ", ListOfResources.Select(s => s.Id.ToString()).ToArray());

(Да, вызов ToArray () раздражает, но Join - очень старый метод, предшествующий LINQ.)

Конечно, «лучшая» версия Join может быть использована следующим образом:

return ListOfResources.Select(s => s.Id).Join(", ");

Реализация довольно проста, но, опять же, использование StringBuilder (для повышения производительности) делает это необходимым.

public static String Join<T>(this IEnumerable<T> items, String delimiter) {
    if (items == null)
        throw new ArgumentNullException("items");
    if (delimiter == null)
        throw new ArgumentNullException("delimiter");

    var strings = items.Select(item => item.ToString()).ToList();
    if (strings.Count == 0)
        return string.Empty;

    int length = strings.Sum(str => str.Length) +
                 delimiter.Length * (strings.Count - 1);
    var result = new StringBuilder(length);

    bool first = true;

    foreach (string str in strings) {
        if (first)
            first = false;
        else
            result.Append(delimiter);
        result.Append(str);
    }

    return result.ToString();
}

1) Не глядя на реализацию в отражателе, я бы предположил, что String.Join выполняет первый проход по строкам, чтобы определить общую длину. Это можно использовать для инициализации StringBuilder соответственно, тем самым экономя дорогостоящие операции копирования в дальнейшем.

РЕДАКТИРОВАТЬ от SLaks : Вот справочный источник для соответствующей части String.Join из .Net 3.5:

string jointString = FastAllocateString( jointLength );
fixed (char * pointerToJointString = &jointString.m_firstChar) {
    UnSafeCharBuffer charBuffer = new UnSafeCharBuffer( pointerToJointString, jointLength); 

    // Append the first string first and then append each following string prefixed by the separator. 
    charBuffer.AppendString( value[startIndex] ); 
    for (int stringToJoinIndex = startIndex + 1; stringToJoinIndex <= endIndex; stringToJoinIndex++) {
        charBuffer.AppendString( separator ); 
        charBuffer.AppendString( value[stringToJoinIndex] );
    }
    BCLDebug.Assert(*(pointerToJointString + charBuffer.Length) == '\0', "String must be null-terminated!");
} 
3
ответ дан 30 November 2019 в 02:36
поделиться

В общем, да, но есть конкретные случаи, которые чрезвычайно сложны. Например, следующий код в общем случае не портируется на LINQ выражение без большого объема взлома.

var list = new List<Func<int>>();
foreach ( var cur in (new int[] {1,2,3})) {
  list.Add(() => cur);
}

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

Примечание. Вышеуказанный код является а не эквивалентным следующему LINQ выражению.

var list = Enumerable.Range(1,3).Select(x => () => x).ToList();

Пример фораха создает список объектов Func, которые все возвращают 3. Версия LINQ создает список Func, которые возвращают 1,2 и 3 соответственно. Именно это и делает этот стиль захвата сложным для переноса.

15
ответ дан 30 November 2019 в 02:36
поделиться

Специфический цикл в вашем вопросе может быть выполнен декларативно следующим образом:

var result = ListOfResources
            .Select<Resource, string>(r => r.Id.ToString())
            .Aggregate<string, StringBuilder>(new StringBuilder(), (sb, s) => sb.Append(sb.Length > 0 ? ", " : String.Empty).Append(s))
            .ToString(); 

Что касается производительности, то можно ожидать падения производительности, но это приемлемо для большинства приложений.

2
ответ дан 30 November 2019 в 02:36
поделиться

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

resource => { if (sb.Length != 0) sb.Append(", "); sb.Append(resource.Id); }

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

.
1
ответ дан 30 November 2019 в 02:36
поделиться

Я думаю, что наиболее важным здесь является то, что во избежание семантической путаницы ваш код должен быть внешне функциональным только тогда, когда он собственно функционален. Другими словами, не используйте побочные эффекты в выражениях LINQ.

2
ответ дан 30 November 2019 в 02:36
поделиться

Технически да.

Любой цикл foreach можно преобразовать в LINQ с помощью метода расширения ForEach , такого как метод в MoreLinq .

Если вы хотите использовать только «чистый» LINQ (только встроенные методы расширения), вы можете злоупотребить методом расширения Aggregate , например так:

foreach(type item in collection { statements }

type item;
collection.Aggregate(true, (j, itemTemp) => {
    item = itemTemp;
    statements
    return true;
);

Это будет правильно обрабатывать любой цикл foreach, даже ответ JaredPar. РЕДАКТИРОВАТЬ : если он не использует параметры ref / out , небезопасный код или yield return .
Не смейте использовать этот трюк в реальном коде.


В вашем конкретном случае вы должны использовать метод расширения строки Join , например этот:

///<summary>Appends a list of strings to a StringBuilder, separated by a separator string.</summary>
///<param name="builder">The StringBuilder to append to.</param>
///<param name="strings">The strings to append.</param>
///<param name="separator">A string to append between the strings.</param>
public static StringBuilder AppendJoin(this StringBuilder builder, IEnumerable<string> strings, string separator) {
    if (builder == null) throw new ArgumentNullException("builder");
    if (strings == null) throw new ArgumentNullException("strings");
    if (separator == null) throw new ArgumentNullException("separator");

    bool first = true;

    foreach (var str in strings) {
        if (first)
            first = false;
        else
            builder.Append(separator);

        builder.Append(str);
    }

    return builder;
}

///<summary>Combines a collection of strings into a single string.</summary>
public static string Join<T>(this IEnumerable<T> strings, string separator, Func<T, string> selector) { return strings.Select(selector).Join(separator); }
///<summary>Combines a collection of strings into a single string.</summary>
public static string Join(this IEnumerable<string> strings, string separator) { return new StringBuilder().AppendJoin(strings, separator).ToString(); }
2
ответ дан 30 November 2019 в 02:36
поделиться
Другие вопросы по тегам:

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