#c# #linq
#c# #linq
Вопрос:
У меня есть объект Message, который переносит формат сообщения, который я не контролирую. Формат представляет собой простой список пар Ключ / значение. Я хочу извлечь список пользователей из данного сообщения. Например, дано следующее сообщение…
1. 200->....
2. 300->....
3. ....
4. 405->....
5. 001->first_user_name
6. 002->first_user_phone
7. 003->first_user_fax
8. 001->second_user_name
9. 001->third_user_name
10. 002->third_user_phone
11. 003->third_user_fax
12. 004->third_user_address
13. .....
14. 001->last_user_name
15. 003->last_user_fax
Я хочу извлечь четырех пользователей с предоставленным набором свойств. Начальные ключи 200/300 ….405 представляют поля, которые мне не нужны, и которые можно пропустить, чтобы получить доступ к пользовательским данным.
Данные каждого пользователя представлены в последовательных полях, но количество полей варьируется в зависимости от того, сколько информации известно о пользователе. Следующий метод делает то, что я ищу. Он использует перечисление возможных типов ключей и метод для нахождения индекса первого поля с пользовательскими данными.
private List<User> ParseUsers( Message message )
{
List<User> users = new List<User>( );
User user = null; String val = String.Empty;
for( Int32 i = message.IndexOfFirst( Keys.Name ); i < message.Count; i )
{
val = message[ i ].Val;
switch( message[ i ].Key )
{
case Keys.Name:
user = new User( val );
users.Add( user );
break;
case Keys.Phone:
user.Phone = val;
break;
case Keys.Fax:
user.Fax = val;
break;
case Keys.Address:
user.Address = val;
break;
default:
break;
}
}
return users;
}
Мне интересно, возможно ли заменить метод запросом Linq. У меня возникли проблемы с тем, чтобы сообщить Linq выбрать нового пользователя и заполнить его поля всеми соответствующими данными, пока вы не найдете начало следующей записи пользователя.
Примечание: относительные номера ключей являются случайными (не 1,2,3,4) в реальном формате сообщения.
Комментарии:
1. используете ли вы Resharper? он довольно хорош в рефакторинге циклов для выражений LINQ.
2. В чем было бы преимущество превращения этого в запрос LINQ? Ваш код выглядит хорошо для меня таким, какой он есть.
3. @мариан.. спасибо за предложение, я не использовал Resharper, но я посмотрю
4. @dtb .. спасибо.. код работает как есть, но каждый раз, когда я просматриваю его файл, у меня возникает раздражающее чувство, что его можно заменить парой строк Linq
Ответ №1:
Я не вижу пользы в изменении вашего кода на запрос LINQ, но это определенно возможно:
private List<User> ParseUsers(Message message)
{
return Enumerable
.Range(0, message.Count)
.Select(i => message[i])
.SkipWhile(x => x.Key != Keys.Name)
.GroupAdjacent((g, x) => x.Key != Keys.Name)
.Select(g => g.ToDictionary(x => x.Key, x => x.Val))
.Select(d => new User(d[Keys.Name])
{
Phone = d.ContainsKey(Keys.Phone) ? d[Keys.Phone] : null,
Fax = d.ContainsKey(Keys.Fax) ? d[Keys.Fax] : null,
Address = d.ContainsKey(Keys.Address) ? d[Keys.Address] : null,
})
.ToList();
}
используя
static IEnumerable<IEnumerable<T>> GroupAdjacent<T>(
this IEnumerable<T> source, Func<IEnumerable<T>, T, bool> adjacent)
{
var g = new List<T>();
foreach (var x in source)
{
if (g.Count != 0 amp;amp; !adjacent(g, x))
{
yield return g;
g = new List<T>();
}
g.Add(x);
}
yield return g;
}
Комментарии:
1. 1: отвечает на вопрос OP и довольно убедительно говорит ему, чтобы он оставил свой прекрасный код как есть.
2. @dtb.. да, отредактированная версия работает хорошо .. ха-ха .. не совсем уверен, что я точно понимаю, как именно, но еще раз спасибо .. приятно видеть, что всегда есть способ что-то сделать .. и я узнаю больше о Linq, просматривая ваш код
Ответ №2:
Нет, и причина в том, что, как правило, большинство функций LINQ, так же, как и SQL-запросы, имеют дело с неупорядоченными данными, т. Е. Они не делают предположений о порядке входящих данных. Это дает им гибкость для распараллеливания и т. Д. Ваши данные имеют внутренний порядок, поэтому не соответствуют модели запроса.
Комментарии:
1. @tim .. да, я думал об этом, когда писал цикл, но каждый раз, когда я пропускал его, у меня возникало неприятное ощущение, что это возможно..
Ответ №3:
Как насчет разделения сообщения на a List<List<KeyValuePait<int, string>>>
, где каждый List<KeyValuePair<int, string>>
представляет одного пользователя. Затем вы могли бы сделать что-то вроде:
// SplitToUserLists would need a sensible implementation.
List<List<KeyValuePair<int,string>>> splitMessage = message.SplitToUserLists();
IEnumerable<User> users = splitMessage.Select(ConstructUser);
С помощью
private User ConstructUser(List<KeyValuePair<int, string>> userList)
{
return userList.Aggregate(new User(), (user, keyValuePair) => user[keyValuePair.Key] = keyValuePair.Val);
}
Комментарии:
1. @joey.. привет, Джоуи.. спасибо за ваш пост .. возился с кодом dtb.. который теперь работает (спасибо dtb).. Мне пришлось бы переопределить свой пользовательский класс для работы с ConstructUser как есть, и я мог бы таким образом читать сообщение более одного раза .. но интересно увидеть разные подходы! еще раз спасибо
2. Нет проблем. Я думаю, что лучшим решением по-прежнему остается то, с которого вы начали. Хотя, возможно, попробуйте преобразовать коммутатор в User или какой-либо объект UserBuilder.
3. 1 .. да, это хорошее предложение.. передайте ему перечислимое количество полей, разделенных, как вы предложили / через GroupAdjacent или аналогичный, и пусть он позаботится о своем собственном создании, пользовательский формат вряд ли изменится (Гм!)… поэтому я думаю, что я бы выбрал User, а не UserFactory / UserBuilder, но даже в этом случае было бы лучше поместить код создания в User, чем иметь его внутри коммутатора в другом месте. Спасибо, Джоуи.
Ответ №4:
Я не думаю, что есть какое-либо преимущество в производительности, но, на мой взгляд, это значительно повышает читаемость.
Возможное решение может выглядеть следующим образом:
var data = File.ReadAllLines("data.txt")
.Select(line => line.Split(new[] {"->"}, StringSplitOptions.RemoveEmptyEntries))
.GroupByOrder(ele => ele[0]);
Настоящая магия происходит за GroupByOrder, который является методом расширения.
public static IEnumerable<IEnumerable<T>> GroupByOrder<T, K>(this IEnumerable<T> source, Func<T, K> keySelector) where K : IComparable {
var prevKey = keySelector(source.First());
var captured = new List<T>();
foreach (var curr in source) {
if (keySelector(curr).CompareTo(prevKey) <= 0) {
yield return captured;
captured = new List<T>();
}
captured.Add(curr);
}
yield return captured;
}
(Отказ от ответственности: идея украдена у Томаса Петричека)
Ваш образец данных выдает следующие группы, которые теперь просто нужно проанализировать в вашем пользовательском объекте.
User:
first_user_name
first_user_phone
first_user_fax
User:
second_user_name
User:
third_user_name
third_user_phone
third_user_fax
third_user_address
User:
last_user_name
last_user_fax
Комментарии:
1. привет, fjdumont.. спасибо за публикацию.. Я по-прежнему предпочитаю ответ dtb, поскольку он является полным ie. принимает ввод сообщения и возвращает список пользователей. Кроме того, ваше решение, похоже, ломается, когда не пользовательские данные present..ie оригинальный пример, похоже, работает только с удаленными полями 200/300/405
2. @fjdumont.. у вас есть ссылка на обсуждение этого Томаса Петричека?
3. Мне пришлось самому погуглить, но вот оно: tomasp.net/blog/custom-linq-grouping.aspx
4. Действительно, спасибо.. jeees: -[ Сейчас мне плохо, но я решил принять ответ dtb.. На самом деле я нашел ваши имена переменных более интуитивно понятными, но пример dtb является полным правильным.. в то время как ваш пример не завершен GroupByOrder всегда возвращает начальную пустую группу и основан на ключах, имеющих возрастающие числовые значения … оба быстрых исправления, но мне пришлось придираться, чтобы я мог выбрать один над другим .. но все равно очень ценится.