Подтвердить что ты не робот

Можно ли заменить все "для" циклов оператором 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

4b9b3361

Ответ 1

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

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

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

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

Ответ 2

В общем, да, но есть конкретные случаи, которые чрезвычайно сложны. Например, следующий код в общем случае не переносит выражение 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();

Образец foreach создает список объектов Func<int>, которые все возвращают 3. Версия LINQ создает список Func<int>, которые возвращают 1,2 и 3 соответственно. Именно это затрудняет перенос этого стиля захвата.

Ответ 3

Фактически, ваш код делает что-то принципиально очень функциональное, а именно, сводит список строк к одной строке, объединяя элементы списка. Единственное обязательное условие для кода - использование 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) Не посмотрев на реализацию в рефлекторе, Id догадался, что 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!");
} 

Ответ 4

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

Любой цикл 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. EDIT: если он не использует параметры 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(); }

Ответ 5

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

Ответ 6

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

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

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

Ответ 7

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

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(); 

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