Заменить цикл для переключения запросом Linq

#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 всегда возвращает начальную пустую группу и основан на ключах, имеющих возрастающие числовые значения … оба быстрых исправления, но мне пришлось придираться, чтобы я мог выбрать один над другим .. но все равно очень ценится.