Действительно ли возможно записать следующий '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
Конечно. Черт возьми, вы можете заменить арифметику на запросы LINQ:
http://blogs.msdn.com/ericlippert/archive/2009/12/07/query-transformations-are-syntactic.aspx
Но вы не должны.
Назначение выражения запроса состоит в том, чтобы представить операцию запроса . Назначение цикла "for" - выполнить итерацию по определенному выражению, чтобы побочные эффекты выполнялись несколько раз. Часто они сильно отличаются друг от друга. Я рекомендую заменять циклы, целью которых является только запрос данных, на более высокоуровневые конструкции, которые более четко опрашивают данные. Настоятельно не рекомендую заменять код, генерирующий побочные эффекты, на понимание запросов, хотя это возможно.
Фактически, ваш код делает что-то, что принципиально очень функционально, а именно сокращает список строк до одной строки, объединяя элементы списка . Единственное, что необходимо в коде, - это использование 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!");
}
В общем, да, но есть конкретные случаи, которые чрезвычайно сложны. Например, следующий код в общем случае не портируется на 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 соответственно. Именно это и делает этот стиль захвата сложным для переноса.
Специфический цикл в вашем вопросе может быть выполнен декларативно следующим образом:
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();
Что касается производительности, то можно ожидать падения производительности, но это приемлемо для большинства приложений.
В общем, вы можете написать выражение лямбда, используя делегата, который представляет тело форач-цикла, в вашем случае что-то вроде :
resource => { if (sb.Length != 0) sb.Append(", "); sb.Append(resource.Id); }
, а затем просто использовать в методе расширения ForEach. Хорошая ли это идея, зависит от сложности тела, в случае, если оно слишком большое и сложное, вы, вероятно, ничего не получите от него, кроме возможной путаницы ;)
.Я думаю, что наиболее важным здесь является то, что во избежание семантической путаницы ваш код должен быть внешне функциональным только тогда, когда он собственно функционален. Другими словами, не используйте побочные эффекты в выражениях LINQ.
Технически да.
Любой цикл 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(); }