Операции множеств DataRow

21

»» В ДАННОЙ СТАТЬЕ ИСПОЛЬЗУЕТСЯ ИСХОДНЫЙ КОД ДЛЯ ПРИМЕРОВ

Как вы, вероятно, помните, в API-интерфейсе LINQ to Objects есть группа стандартных операций запросов, предназначенных для выполнения операций множеств над последовательностями объектов. Имеются в виду операции Distinct, Except, Intersect, Union и SequenceEqual. Каждая из этих операций выполняет одну из операций множеств над двумя последовательностями.

Каждой из этих операций для выполнения соответствующей операции с множествами необходим способ определения эквивалентности двух элементов множеств. Они осуществляют сравнение элементов, вызывая методы GetHashCode и Equals над элементами.

Для DataRow это означает сравнение ссылок, что является нежелательным поведением. Это приведет к неправильному определению эквивалентности элементов, что заставит операции возвращать некорректные результаты. Из-за этого каждая из таких операций имеет дополнительный прототип, который был опущен в статьях, посвященных LINQ to Objects. Этот дополнительный прототип позволяет предоставлять в качестве аргумента объект IEqualityComparer. Удобно, что объект-компаратор по умолчанию — System.Data.DataRowComparer.Default — предназначен специально для этих версий операций. Этот класс-компаратор находится в пространстве имен System.Data в сборке System.Data.Entity.dll.

Этот компаратор определяет эквивалентность элементов, сравнивая количество столбцов и статический тип данных каждого из них, а также используя интерфейс IComparable на динамическом типе данных столбца, если тип реализует этот интерфейс. В противном случае он вызывает статический метод Equals класса System.Object.

Каждый из этих дополнительных прототипов операций определен в статическом классе System.Linq.Enumerable, наряду со всеми остальными прототипами этих операций.

В этой статье будет показано несколько примеров, чтобы проиллюстрировать неправильный и, что более важно —- правильный способ выполнения сравнений последовательностей при работе с объектами DataSet.

Distinct

Операция Distinct исключает дублированные строки из последовательности объектов. Она возвращает объект, который при перечислении исходной последовательности возвращает последовательность объектов с исключенными дублированными строками. Обычно эта операция определяет дублирование строк, вызывая методы GetHashCode и Equals типа данных каждого из элементов. Однако для объектов типа DataRow это даст неверный результат.

Поскольку будет вызван дополнительный прототип и предоставлен объект-компаратор System.Data.DataRowComparer.Default, эквивалентность элементов будет определена правильно. С его помощью дублирование строк определяется сравнением объектов DataRow, используя количество столбцов в строке и статический тип данных каждого столбца, а затем используя интерфейс IComparable на каждом столбце, если его динамический тип данных реализует этот интерфейс, либо вызывая статический метод Equals класса System.Object в противном случае.

Операция Distinct имеет один прототип, описанный ниже:

public static IEnumerable<T> Distinct<T> ( 
        this IEnumerable<T> source, 
        IEqualityComparer<T> comparer);

В следующем примере объект DataTable создается из массива объектов Student с использованием общего метода GetDataTable и массива, содержащего в себе один дубликат. В массиве дублируется запись со значением Id равным 1. Затем содержимое DataTable отображается. Это доказывает, что запись встречается в DataTable два раза. После этого вызовом операции Distinct удаляются любые дублированные строки, и DataTable снова отображается, показывая, что дублированные строки удалены:

Student[] students = {
                new Student { Id=1, Name="Александр Ерохин"},
                new Student { Id=5, Name="Елена Волкова"},
                new Student { Id=9, Name="Дмитрий Моисеенко"},
                new Student { Id=1, Name="Александр Ерохин"},
                new Student { Id=16, Name="Андрей Мухамедшин"},
                new Student { Id=20, Name="Ирина Сас"},
                new Student { Id=28, Name="Артем Амурин"}
                                 };

            DataTable dt = GetDataTable(students);

            Console.WriteLine("{0}Перед вызовом Distinct(){0}",
               System.Environment.NewLine);

            OutputDataTableHeader(dt, 15);

            foreach (DataRow dataRow in dt.Rows)
            {
                Console.WriteLine("{0,-15}{1,-15}",
                  dataRow.Field<int>(0),
                  dataRow.Field<string>(1));
            }

            IEnumerable<DataRow> distinct =
              dt.AsEnumerable().Distinct(DataRowComparer.Default);

            Console.WriteLine("{0}После вызова Distinct(){0}",
              System.Environment.NewLine);

            OutputDataTableHeader(dt, 15);

            foreach (DataRow dataRow in distinct)
            {
                Console.WriteLine("{0,-15}{1,-15}",
                  dataRow.Field<int>(0),
                  dataRow.Field<string>(1));
            }

Обратите внимание, что для получения последовательности объектов DataRow из DataTable используется операция AsEnumerable, потому что именно на этой последовательности должна быть вызвана операция Distinct. Также заметьте, что в массиве students запись с Id = 1 повторяется.

Как видите, на объекте DataRow вызывается метод по имени Field. Пока достаточно понимать, что это вспомогательный метод, который делает получение значения объекта DataColumn из DataRow более удобным. В статье "Операции над полями DataRow" далее операция Field<T> будет описана более подробно.

Вот результат:

Операция Distinct с компаратором эквивалентности

Обратите внимание, что в результатах перед вызовом операции Distinct запись со значением Id = 1 повторялась, а после вызова Distinct второе ее появление было удалено.

Except

Операция Except предоставляет последовательность объектов DataRow, которые есть в первой последовательности объектов DataRow, но отсутствуют во второй последовательности объектов DataRow. Операция возвращает объект, который при перечислении проходит по второй последовательности объектов DataRow, собирая уникальные элементы, затем проходит по первой последовательности объектов DataRow, удаляя те из них, которые обнаружены во второй последовательности; сгенерированный результат возвращается.

Чтобы определить уникальность элементов в одной последовательности, а также то, что один элемент из первой последовательности не эквивалентен элементу во второй последовательности, операция должна иметь возможность определять их эквивалентность. Обычно эта операция определяет эквивалентность, вызывая методы GetHashCode и Equals типа данных каждого элемента. Однако для объектов типа DataRow это может дать некорректный результат.

Поскольку планируется вызывать дополнительный прототип и предоставить объект-компаратор System.Data.DataRowComparer.Default, эквивалентность элементов будет определена правильно. С его помощью дублирование строк определяется сравнением объектов DataRow, используя количество столбцов в строке и статический тип данных каждого столбца, а затем используя интерфейс IComparable на каждом столбце, если его динамический тип данных реализует этот интерфейс, либо вызывая статический метод Equals класса System.Object в противном случае.

Операция Except имеет один прототип, описанный ниже:

public static IEnumerable<T> Except<T> ( 
    this IEnumerable<T> first, 
    IEnumerable<T> second, 
    IEqualityComparer<T> comparer);

В следующем примере операция Except вызывается дважды. Первый раз ей передается объект-компаратор System.Data.DataRowComparer.Default, так что результаты первого запроса с помощью операции Except будут корректными. При втором вызове операции Except объект-компаратор не передается. Это приведет к некорректному результатам запроса. Ниже показан код:

Student[] students1 = { 
               new Student { Id = 1, Name = "Александр Ерохин" },
               new Student { Id = 7, Name = "Елена Волкова" },
               new Student { Id = 13, Name = "Дмитрий Моисеенко" },
               new Student { Id = 72, Name = "Андрей Мухамедшин" }
             };

            Student[] students2 = { 
               new Student { Id = 5, Name = "Ирина Сас" },
               new Student { Id = 7, Name = "Елена Волкова" },
               new Student { Id = 29, Name = "Артем Амурин" },
               new Student { Id = 72, Name = "Андрей Мухамедшин" }
             };

            DataTable dt1 = GetDataTable(students1);
            IEnumerable<DataRow> seq1 = dt1.AsEnumerable();
            DataTable dt2 = GetDataTable(students2);
            IEnumerable<DataRow> seq2 = dt2.AsEnumerable();

            IEnumerable<DataRow> except =
              seq1.Except(seq2, System.Data.DataRowComparer.Default);

            Console.WriteLine("{0}Результаты Except() с компаратором{0}",
              System.Environment.NewLine);

            OutputDataTableHeader(dt1, 15);

            foreach (DataRow dataRow in except)
            {
                Console.WriteLine("{0,-15}{1,-15}",
                  dataRow.Field<int>(0),
                  dataRow.Field<string>(1));
            }

            except = seq1.Except(seq2);

            Console.WriteLine("{0}Результаты Except() без компаратора{0}",
              System.Environment.NewLine);

            OutputDataTableHeader(dt1, 15);

            foreach (DataRow dataRow in except)
            {
                Console.WriteLine("{0,-15}{1,-15}",
                  dataRow.Field<int>(0),
                  dataRow.Field<string>(1));
            }

Здесь создаются два объекта DataTable, которые наполняются данными из массивов Student. С помощью метода AsEnumerable из каждого объекта DataTable создается последовательность. Затем вызывается операция Except с передачей ей объекта-компаратора System.Data.DataRowComparer.Default. Второй раз Except вызывается без указания объекта-компаратора.

Посмотрим на результат работы этого кода:

Операция Except с компаратором эквивалентности и без

Как видите, операция Except, вызванная с объектом-компаратором System.Data.DataRowComparer.Default, способна правильно определить эквивалентность элементов в двух последовательностях, в то время как операции Except, вызванной без объекта-компаратора, это не удается.

Intersect

Операция Intersect порождает последовательность объектов DataRow, которая представляет собой пересечение двух последовательностей объектов DataRow. Она возвращает объект, который при перечислении проходит по второй последовательности, собирая уникальные элементы, а затем проходит по первой последовательности, возвращая элементы, присутствующие в обеих последовательностях.

Чтобы определить уникальность элементов одной последовательности, а также эквивалентность элемента одной последовательности элементу другой последовательности, операция должна уметь определять эквивалентность элементов. Обычно эта операция определяет эквивалентность элементов вызовом методов GetHashCode и Equals типа данных каждого элемента. Однако для объектов типа DataRow это может дать неправильный результат.

Поскольку планируется вызывать дополнительный прототип и предоставить объект-компаратор System.Data.DataRowComparer.Default, эквивалентность элементов будет определена правильно.

Операция Intersect имеет один прототип, описанный ниже:

public static IEnumerable<T> Intersect<T> ( 
         this IEnumerable<T> first, 
         IEnumerable<T> second, 
         IEqualityComparer<T> comparer);

В следующем примере используется тот же базовый код, который применялся в примере Except, с заменой вызова операции Except на Intersect:

...
IEnumerable<DataRow> except =
              seq1.Intersect(seq2, System.Data.DataRowComparer.Default);
              
  ...
except = seq1.Intersect(seq2);

Ниже показан вывод этого примера:

Операция Intersect с компаратором эквивалентности и без

Как видите, операция Intersect с компаратором способна правильно определить эквивалентность элементов из двух последовательностей, в то время как операция Intersect без компаратора — нет.

Union

Операция Union производит последовательность объектов DataRow, которая представляет собой объединение двух последовательностей объектов DataRow. Она возвращает объект, который при перечислении проходит по первой последовательности объектов DataRow, затем по второй последовательности объектов DataRow, возвращая любой объект, который не содержался в первой последовательности.

Операция Union имеет один прототип, описанный ниже:

public static IEnumerable<Т> Union<T> ( 
        this IEnumerable<T> first, 
        IEnumerable<T> second, 
        IEqualityCoraparer<T> comparer);

В этом примере используется тот же базовый код, что и в примере Intersect, но вызов операции Intersect заменяется Union:

...
IEnumerable<DataRow> except =
              seq1.Union(seq2, System.Data.DataRowComparer.Default);

...
except = seq1.Union(seq2);
Операция Union с компаратором эквивалентности и без

Обратите внимание, что результат выполнения операции Union с объектом-компаратором корректен, но без объекта-компаратора — нет.

SequenceEqual

Операция SequenceEqual сравнивает две последовательности объектов DataRow, чтобы определить, эквивалентны ли они. Она выполняет перечисление двух исходных последовательностей, сравнивая соответствующие объекты DataRow. Если две исходные последовательности имеют одинаковое количество записей, и все соответствующие объекты DataRow в них эквивалентны, возвращается true. В противном случае две последовательности считаются не эквивалентными, и возвращается false.

Операция SequenceEqual имеет один прототип, который описан ниже:

public static bool SequenceEqual<T> ( 
    this IEnumerable<T> first, 
    IEnumerable<T> second, 
    IEqualityComparer<T> comparer);

В примере ниже строятся две идентичные последовательности объектов DataRow, которые сравниваются с помощью операции SequenceEqual с передачей ей объекта-компаратора и без передачи этого объекта. При вызове SequenceEqual с компаратором возвращается true, что указывает на эквивалентность двух последовательностей, а при вызове той же операции без компаратора возвращается значение false, указывающее на то, что последовательности не эквивалентны:

Student[] students = {
                new Student { Id=1, Name="Александр Ерохин"},
                new Student { Id=5, Name="Елена Волкова"},
                new Student { Id=9, Name="Дмитрий Моисеенко"},
                new Student { Id=1, Name="Александр Ерохин"},
                new Student { Id=16, Name="Андрей Мухамедшин"},
                new Student { Id=20, Name="Ирина Сас"},
                new Student { Id=28, Name="Артем Амурин"}
                                 };

            DataTable dt1 = GetDataTable(students);
            IEnumerable<DataRow> seq1 = dt1.AsEnumerable();
            DataTable dt2 = GetDataTable(students);
            IEnumerable<DataRow> seq2 = dt2.AsEnumerable();

            bool equal = seq1.SequenceEqual(seq2, System.Data.DataRowComparer.Default);
            Console.WriteLine("SequenceEqual() с компаратором : {0}", equal);

            equal = seq1.SequenceEqual(seq2);
            Console.WriteLine("SequenceEqual() без компаратора : {0}", equal);

Первый вызов должен сообщить, что две последовательности эквиваленты, а второй — что нет. Результат в точности соответствует ожиданиям:

Операция SequenceEqual
Пройди тесты
Лучший чат для C# программистов