Вопрос о проектировании класса: объединение списка и AllStats

#c# #linq #class

#c# #linq #класс

Вопрос:

У меня есть класс Player и класс Stat. Класс Player имеет свойство List, где PlayerStat имеет свойства List и XP. Я думаю, что мой дизайн несовершенен, потому что у меня возникают проблемы с выполнением вещей, которые, по моему мнению, должны быть простыми. Я хочу вывести список всех игроков и их опыта для всей статистики. Ниже приведены более подробные сведения о классах и методе GetCompletePlayerStats, который мне действительно не нравится. В принципе, мне нужно указать XP для всей статистики игрока, если у игрока нет статистики, то у него должен быть XP, равный нулю. Будем признательны за любую помощь / предложения по дизайну.

 public class Stat : EntityBase{
  public virtual string Name { get; set; }
  public virtual UnitOfMeasure Unit { get; set; }
  public virtual int UnitXP { get; set; }
}
public class Player : EntityBase{
  public virtual string Name { get; set; }
  public virtual IList<PlayerStat> PlayerStats { get; set; }

  public virtual List<PlayerStat> GetCompletePlayerStats(IQueryable<Stat> stats)
    {
        var allStats = new List<PlayerStat>();
        var playerStatIds = PlayerStats.Select(ps => ps.PlayerStatistic.Id).ToList();
        if (playerStatIds.Count == 0)
        {
            allStats.AddRange(stats.Select(stat => new PlayerStat() {PlayerStatistic = stat, XP = 0}));
        }
        else
        {
            var zeroStats = stats.Where(s => !playerStatIds.Contains(s.Id)).ToList();

            allStats.AddRange(zeroStats.Select(zeroStat => new PlayerStat() {PlayerStatistic = zeroStat, XP = 0}));
            allStats.AddRange(PlayerStats);
        }

        return allStats;
    }

}
public class PlayerStat : EntityBase{
  public virtual Stat PlayerStatistic { get; set; }
  public virtual double XP { get; set; }
}
  

Ответ №1:

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

Однако вы сказали, что это то GetCompletePlayerStats , что вам действительно не нравится. Мне пришлось прочитать это несколько раз, чтобы понять, что вы пытаетесь здесь сделать. Если я правильно понял, вы просто хотите вернуть PlayerStat объект, соответствующий каждому заданному Stat объекту. Я предполагаю, что Stat есть Id поле (вы используете его в своем методе) для сравнения двух из них на предмет семантического равенства.

Учитывая, что до сих пор я делал правильные предположения (к сожалению, вы не предоставили много информации), метод может быть упрощен до чего-то вроде:

 public virtual IEnumerable<PlayerStat> GetCompletePlayerStats(IEnumerable<Stat> stats)
{
    foreach(Stat stat in stats)
        yield return PlayerStats.FirstOrDefault(s => stat.Id == s.PlayerStatistic.Id) ?? 
                     new PlayerStat() {PlayerStatistic = stat, XP = 0};
    yield break;
}
  

Этот метод здесь не требует, чтобы a IQueryable а скорее IEnumerable выполнял итерацию через foreach цикл и выдавал соответствующий PlayerStats объект, если таковой имеется, или создавал новый с XP, установленным в 0, в противном случае (оператор объединения нулей ?? очень полезен в этих случаях).

Надеюсь, это поможет!

Комментарии:

1. Да, это сработало бы, хотя у меня есть некоторые проблемы с этим: во-первых, вызов FirstOrDefault() в цикле эффективно выполняет объединение вложенного цикла (один явный цикл и другой цикл внутри FirstOrDefault), которое имеет действительно плохую производительность с большими наборами. Для небольшого количества статистических данных это не будет иметь значения, но я слишком часто попадался в такого рода ловушки, поэтому я просто стараюсь избегать их, когда могу. Во-вторых, он выдает отложенную перечислимую, которая будет повторно оцениваться при каждом вызове, что может быть не тем, что вы хотите. И, в-третьих, разрыв yield в качестве последнего оператора в методе является избыточным.

2. @Avish: Вы правы, yield break; действительно избыточно. Но я не совсем согласен с вами по поводу ленивой перечислимости: вы говорите, что это может быть не то, чего хочет op, но почему вы можете предположить, что создание списка — это то, чего хочет op? У нас нет никакой информации об этом. Если мы работаем с большими наборами данных (как вы сказали), ленивое перечисление может быть гораздо более подходящим, если он перечисляет результаты только один раз. Если он хочет повторно использовать результирующий набор, он все равно может вызвать его следующим образом: GetCompletePlayerStats(stats).ToList() . Это более гибко, чем просто возвращать IList<> , оно предполагает то, чего может не хотеть вызывающий.

3. @Avish: Ваше решение имеет лучшую скорость роста: создание словаря занимает O (m) времени, но впоследствии каждый ContainsKey вызов выполняется в O (1). Таким образом, общий метод находится в O (m n), тогда как мое решение использует гораздо худший O (m n). Однако я не верю, что мы работаем с достаточно большими наборами данных, чтобы оправдать это в первую очередь. То, что вы здесь делаете, является преждевременной оптимизацией. С небольшими наборами данных мой алгоритм может быть даже быстрее (не тестировал его — просто предположение). Однако я хочу сказать: обычно лучше оптимизировать с помощью профилировщика * впоследствии , если возникнут какие-либо проблемы с производительностью…

4. @Philip: вы абсолютно правы по обоим пунктам — ленивый перечисляемый более гибкий, чем список, и создание словаря в нем является преждевременной оптимизацией. Тем не менее, это оба примера паттернов, которых я стараюсь избегать, поскольку они могут легко вернуться, чтобы укусить вас позже, в более сложных ситуациях, а вы этого не заметите. Я считаю, что общие принципы «не предоставляйте ленивые перечислимые, если вы точно не знаете, кто будет их использовать» и «не объединяйте вложенными циклами, когда размер набора данных неизвестен», являются хорошими практическими правилами в общем случае. Конечно, каждый конкретный случай следует оценивать независимо.

5. @Avish: 100% Подтверждение 🙂 Надеюсь, это обсуждение показало оператору, что он должен найти хороший баланс между всеми этими фактами и решить, что лучше всего подходит для его игры.

Ответ №2:

При существующем дизайне этот метод может быть упрощен таким образом:

 public virtual IList<PlayerStat> GetCompletePlayerStats(IEnumerable<Stat> stats)
{
   // build a dictionary of existing stats by ID to facilitate 
   // the join with requested stats (effectively doing a hash join)
   var playerStatsById = PlayerStats.ToDictionary(ps => ps.PlayerStatistic.Id);

   // for each requested stat, return either the corresponding player stat 
   // or a zero stat if one isn't found, maintaining the original order of stats
   return stats
       .Select(s => playerStatsById.ContainsKey(s.Id) ? 
           playerStatsById[s.Id] : 
           new PlayerStat { PlayerStatistic = s, XP = 0 })
       .ToList();
}
  

Обратите внимание, что, поскольку это фактически операция внешнего соединения (вы соединяетесь stats с PlayerStats помощью, но вам также нужно, чтобы несовпадения приводили к нулевому результату), вы также можете сделать это с помощью операции GroupJoin() LINQ , хотя я подозреваю, что это было бы гораздо менее читабельно.

Что меня беспокоит в вашем дизайне, так это двусмысленность имен, например, у вас есть и PlayerStat класс, и PlayerStatistic свойство, и они означают немного разные вещи; подумайте о том, чтобы называть вещи по-другому, и они могут выглядеть лучше; на самом деле, это, вероятно, лучший совет по дизайну, который я могу вам дать, для любой ситуации 🙂