Создание нескольких пользовательских компараторов для класса на основе словаря

Я хочу, чтобы в моем классе возвращался список из словаря, но разрешалась пользовательская сортировка с использованием предварительно написанных методов сравнения. В моем исходном Java-коде, из которого я конвертирую, я создал методы сравнения с использованием Google Guava Ordering в своем классе, а затем у меня был один метод, называемый следующей передачей в одном из общедоступных методов компаратора, объявленный примерно так:

public List<Word> getWords(Comparator c) { }

Я пытаюсь воссоздать это на С#, но я не могу понять, как это сделать. По сути, в приведенном ниже коде вы можете видеть, что есть три версии для каждого типа сортировки, и, кроме того, я создаю два списка для каждого возвращаемого значения, что кажется немного расточительным.

Я посмотрел на создание делегатов, но немного потерялся, затем подумал, что могу создать IComparable, но затем увидел IComparator, а затем увидел, что метод Sort принимает Comparator.

Может ли кто-нибудь указать мне, как наилучшим образом преобразовать это в единый вид «GetWords», позволяя клиентам вызывать GetWords, извлекая отсортированный список из предварительно предоставленного набора заказов.

public partial class WordTable
{
    private Dictionary<string, Word> words;

    public WordTable()
    {
        //for testing
        words = new Dictionary<string, Word>();
        words.Add("B", new Word("B", WordTypes.Adjective));
        words.Add("A", new Word("A", WordTypes.Noun));
        words.Add("D", new Word("D", WordTypes.Verb));
    }
    public List<Word> GetWords()
    {
        return words.Values.ToList();
    }
    public List<Word> GetWordsByName()
    {
        List<Word> list = words.Values.ToList<Word>();
        return list.OrderBy(word => word.Name).ToList();
    }
    public List<Word> GetWordsByType()
    {
        List<Word> list = words.Values.ToList<Word>();
        return list.OrderBy(word => word.Type).ToList();
    }
}

c#
person Neil Walker    schedule 17.10.2018    source источник
comment
Это Comparator в нескольких местах в тексте или на самом деле Comparer?   -  person Dialecticus    schedule 17.10.2018
comment
Я просто смешиваю их в предположении, что вы знаете, что я просто хочу передать какую-то функцию сравнения...   -  person Neil Walker    schedule 17.10.2018


Ответы (3)


Есть два подхода, которые вы можете использовать.

IComparer

Это более тесно связано с вашим прошлым опытом работы с Java.

Официальный способ — использовать IComparer<T> (ссылка).

Подобно вашему Comparator в примере Java, это позволяет вам создавать различные методы сортировки, которые все реализуют интерфейс IComparer<Word>, а затем вы можете динамически выбирать свой метод сортировки.

В качестве простого примера:

public class WordNameComparer : IComparer<Word>
{
    public int Compare(Word word1, Word word2)
    {
         return word1.Name.CompareTo(word2.Name);
    }
}

И тогда вы можете сделать:

public List<Word> GetWords(IComparer<Word> comparer)
{
    return words.Values.OrderBy(x => x, comparer).ToList();
}

Который вы можете вызвать, выполнив:

var table = new WordTable();

List<Word> sortedWords = table.GetWords(new WordNameComparer());

И, конечно же, вы меняете логику сортировки, передавая другой IComparer<Word>.


Функциональные параметры

Судя по опыту, это более предпочтительный подход из-за улучшенной читабельности LINQ и низкой стоимости реализации.

Глядя на ваши последние два метода, вы должны увидеть, что единственная переменная часть — это лямбда-метод, который вы используете для упорядочения данных. Конечно, вы можете превратить эту переменную в параметр метода:

public List<Word> GetWordsBy<T>(Func<Word,T> orderByPredicate)
{
    return words.Values.OrderBy(orderBy).ToList();
}

Поскольку предикат OrderBy использует универсальный параметр для выбранного свойства (например, сортировка по строковому полю? поле int?...), вы должны сделать этот метод универсальным, но вам не нужно явно использовать универсальный параметр, когда вы вызываете метод. Например:

var sortedWordsByName =   table.GetWordsBy(w => w.Name);
var sortedWordsByLength = table.GetWordsBy(w => w.Name.Length);
var sortedWordsByType =   table.GetWordsBy(w => w.Type);

Обратите внимание, что если вы выберете класс, а не тип значения, вам либо придется создать и передать IComparer<> для этого класса, либо сам класс должен реализовать IComparable<>, чтобы его можно было отсортировать так, как вы хотите.

Вы можете ввести порядок по возрастанию/убыванию:

public List<Word> GetWordsBy<T>(Func<Word,T> orderByPredicate, bool sortAscending = true)
{
    return sortAscending
              ? words.Values.OrderBy(orderBy).ToList()
              ? words.Values.OrderByDescending(orderBy).ToList();
}

Обновлять

Я пытался сделать это с помощью делегатов, но избегал того, чтобы вызывающему абоненту приходилось выполнять собственный оператор лямбда и использовать предопределенные.

Вы можете просто обернуть свой метод некоторыми предопределенными параметрами:

public List<Word> GetWordsBy<T>(Func<Word,T> orderByPredicate)
{
    return words.Values.OrderBy(orderBy).ToList();
}

public List<Word> GetWordsByName()
{
    return GetWordsBy(w => w.Name);
}

Таким образом, вашим внешним вызывающим объектам не нужно использовать лямбду, если они этого не хотят; но вы по-прежнему сохраняете преимущества наличия повторно используемого кода внутри вашего класса.

Есть много способов сделать это. Я предпочитаю создавать предустановленные методы для удобочитаемости, но вместо этого вы можете иметь перечисление, которое затем сопоставляется с правильным Func. Или вы можете создать несколько статических предустановленных лямбда-выражений, на которые может ссылаться внешний вызывающий объект. Или... Весь мир - твоя устрица :-)

person Flater    schedule 17.10.2018
comment
Спасибо, IComparer - это близкое сравнение со старой java, но способ Func был тем способом, которым я пытался сделать это с делегатами, но избегая того, чтобы вызывающему абоненту приходилось сворачивать свой собственный лямбда-оператор и использовать предопределенные. - person Neil Walker; 17.10.2018
comment
@NeilWalker: ничто не мешает вам указать предустановленные значения. Я обновлю ответ небольшим примером. - person Flater; 17.10.2018


Я надеюсь, что это работает или даже компилируется.

class WordTable
{
    public List<Word> GetWords(IComparer<Word> comparer)
    {
        return words.Values.OrderBy(x => x, comparer).ToList();
    }
}

class WordsByNameAndThenTypeComparer : IComparer<Word>
{
    public override int Compare(Word x, Word y)
    {
        int byName = x.Name.CompareTo(y.Name);

        return byName != 0 ? byName : x.Type.CompareTo(y.Type);
    }
}

Применение:

WordTable wt = new WordTable();
List<Words> words = wt.GetWords(new WordsByNameAndThenTypeComparer());
person Dialecticus    schedule 17.10.2018
comment
Это не игра в угадайку. Не публикуйте ответы, для которых вы даже не знаете, что они работают или компилируются. - person Flater; 17.10.2018
comment
Пожалуйста, понизьте голос, только если вы точно знаете, что ответ плохой. - person Dialecticus; 17.10.2018
comment
Ответ не дает никаких объяснений, предоставляя менее чем тривиальное решение (вы представляете совершенно новый класс IComparer<T>, а не просто исправляете синтаксическую опечатку), и, кроме того, вы явно утверждаете, что он может даже не работать/компилироваться. Это две веские причины считать это плохим ответом. - person Flater; 17.10.2018
comment
Ответ не плохой. Он компилируется. Это ответ на вопрос. Но твой ответ лучше. Будь добрым. - person Dialecticus; 17.10.2018
comment
На отзыв о вашем ответе никоим образом не повлияло то, что я сам написал ответ. - person Flater; 17.10.2018
comment
Спасибо. Я понял суть того, что вы сделали, по сути, аналогично предыдущему потоку с использованием IComparer. - person Neil Walker; 17.10.2018
comment
@NeilWalker Обычно мы ожидаем от функций стабильных результатов, чтобы один и тот же ввод всегда давал один и тот же вывод. Для этого мы должны сортировать по всем свойствам при сортировке значений Dictionary. Поэтому, даже если вы используете Func вместо IComparer, вы должны сортировать по всем свойствам во всех случаях. - person Dialecticus; 17.10.2018