Delphi Linq
Искал какое-то время аналог C# Linq в Delphi. Встретил много чего интересного, но когда наткнулся на openSource библиотеку Spring4D, понял, что это то, что нужно. В чем профит ?
Дело в том, что в System.Generics.Collections нет, как ни странно, TCollection, которая позволяла бы делать вот такие финты, аналогичные цепочкам в C# Linq
Объявление
1 |
List: IList<TIntegerStringPair>; |
Как видно, здесь объявляется интерфейс, который богат на разнообразные Linq методы и расширяет стандартный функционал System.Generics.Collections
Заполнение
1 2 3 4 5 6 7 8 9 10 11 12 |
Clear; List := TCollections.CreateList<TIntegerStringPair>; List.Add(TIntegerStringPair.Create(1, 'one')); List.Add(TIntegerStringPair.Create(2, 'two')); List.Add(TIntegerStringPair.Create(3, 'three')); List.Add(TIntegerStringPair.Create(4, 'four')); List.Add(TIntegerStringPair.Create(5, 'five')); List.Add(TIntegerStringPair.Create(6, 'six')); List.Add(TIntegerStringPair.Create(7, 'seven')); List.Add(TIntegerStringPair.Create(8, 'eight')); List.Add(TIntegerStringPair.Create(9, 'nine')); List.Add(TIntegerStringPair.Create(10, 'ten')); |
Использование
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
... var Predicate: Spring.TPredicate<TIntegerStringPair>; Pair: TIntegerStringPair; Enumerable: IEnumerable<TIntegerStringPair>; begin Clear; Predicate := function(const Pair: TIntegerStringPair): Boolean begin Result := Pair.Key mod 2 = 0; end; // Same as TWhereEnumerable<TIntegerStringPair>.Create(List, Predicate); Enumerable := List.Where(Predicate).Where( function(const Pair: TIntegerStringPair): Boolean begin Result := Pair.Key > 2; end); for Pair in Enumerable do begin AddToMemo(Pair.Value); end; end; |
Результат
1 2 3 4 |
four six eight ten |
Вау! И это работает. На мой взгляд довольно удобная вещь.
В тестовом примере, рядом с библиотекой
1 |
C:\Work\DSeattle\libs\sglienke-spring4d\Samples\SpringDemos\Demo.Collections\Demo.Spring.Enumerators.dproj |
я встретил следующие примеры методов
Where
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
Clear; Predicate := function(const Pair: TIntegerStringPair): Boolean begin Result := Pair.Key mod 2 = 0; end; // Same as TWhereEnumerable<TIntegerStringPair>.Create(List, Predicate); Enumerable := List.Where(Predicate).Where( function(const Pair: TIntegerStringPair): Boolean begin Result := Pair.Key > 2; end); for Pair in Enumerable do begin AddToMemo(Pair.Value); end; |
Skip
1 2 3 4 5 6 7 8 9 |
Clear; // Skip the first seven elements // The below is basically the same as TSkipEnumerable<TIntegerStringPair>.Create(List, 7); Enumerable := List.Skip(7); for Pair in Enumerable do begin AddToMemo(Pair.Value); end; |
SkipWhile
1 2 3 4 5 6 7 8 9 10 11 12 |
Clear; Predicate := function(const Pair: TIntegerStringPair): Boolean begin Result := Pair.Key < 5; end; // Same as TSkipWhileEnumerable<TIntegerStringPair>.Create(List, Predicate); Enumerable := List.SkipWhile(Predicate); for Pair in Enumerable do begin AddToMemo(Pair.Value); end; |
Take
1 2 3 4 5 6 7 8 9 |
Clear; // Only "take" the first seven // Same as TTakeEnumerable<TIntegerStringPair>.Create(List, 7); Enumerable := List.Take(7); for Pair in Enumerable do begin AddToMemo(Pair.Value); end; |
TakeWhile
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
Clear; Predicate := function(const Pair: TIntegerStringPair): Boolean begin Result := Pair.Key < 5; end; // Same as TTakeWhileEnumerable<TIntegerStringPair>.Create(List, Predicate); // "Takes" the items from the enumeration as long as the Pair.Key is less than // five. Once it isn't less than five, it stops and nothing else is returned. Enumerable := List.TakeWhile(Predicate); for Pair in Enumerable do begin AddToMemo(Pair.Value); end; |
Concat
1 2 3 4 5 6 7 8 9 10 |
Clear; TempList := CreateAnotherList; // Same as TConcatEnumerable<TIntegerStringPair>.Create(List, TempList); Enumerable := List.Concat(TempList); for Pair in Enumerable do begin AddToMemo(Pair.Value); end; |
First
1 2 3 |
Clear; Pair := List.First; AddToMemo(Pair.Value); |
FirstOrDefault
1 2 3 |
Clear; Pair := List.FirstOrDefault; AddToMemo(Pair.Value); |
LastOrDefault
1 2 3 |
Clear; Pair := List.FirstOrDefault; AddToMemo(Pair.Value); |
Last
1 2 3 |
Clear; Pair := List.Last; AddToMemo(Pair.Value); |
ElementAt
1 2 3 |
Clear; Pair := List.ElementAt(4); // zero-based AddToMemo(Pair.Value); |
Min
1 2 3 |
Clear; Pair := List.Min; AddToMemo(Pair.Value); |
Max
1 2 3 |
Clear; Pair := List.Max; AddToMemo(Pair.Value); |
Reversed
1 2 3 4 5 |
Clear; for Pair in List.Reversed do begin AddToMemo(Pair.Value); List. end; |
Foreach
1 2 3 4 5 6 7 |
Clear; Action := procedure(const Pair: TIntegerStringPair) begin AddToMemo(Format('The numeric form of %s is %d', [Pair.Value, Pair.Key])) end; List.ForEach(Action); |
Также есть Any (источник примера)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
uses Spring, Spring.Collections; var M: IList<Integer>; begin M := TCollections.CreateList<Integer>; M.Add(3); M.Add(1); M.Add(2); M.Add(-1); Writeln( M.Any( function (const AElement: Integer): Boolean begin Result := AElement < 0; end)); // Outputs True Readln; end |
Ещё есть такие методы
- All
- IsEmpty
- AsReadonlyList – returns the interfaces for read-only access to the list;
и др.
Таким образом, используя этот более высокий уровень абстракции, можно делать код более компактным и читабельным. Для меня эта находка оказалась ценной.
Nullable types
Простые типы можно сделать Nullable, вот пример
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
uses Spring, System.Variants; var X: Nullable<Integer>; Y: Integer; begin X := Null; Writeln(X.HasValue); // Outputs False X := 10; Writeln(X.Value); // Outputs 10 Writeln(X); // [Error] E2054 Illegal type in Write/Writeln statement X := Null; Y := X; //Raises EInvalidOpException as nullable has no value end. |
Shared<T> type
Это такой слегка недоделанный ISmartPointer, о котором я писал ранее. Смысл в том, что можно создавать переменные и не думать об их уничтожении. Но недоделанность заключается в том, что к членам класса нужно обращаться через Value, но этот момент поправлен в ISmartpointer, вот пример
1 2 3 4 5 6 7 8 9 10 11 |
uses Spring; var L, R: Shared<TStrings>; begin L := TStringList.Create; R := TStringList.Create; ...do something with them... RemoveLeftStringsFromRight(L, R); R.Value.Text end; |
Здесь, память под ссылками L, R почистится в момент выхода из области видимости, либо после присвоения им nil, как я понимаю (поскольку все это реализовано через стандартный механизм Delphi ARC, о котором я тоже писал ранее)
SortedList vs List
Нашёл в сети интересный пример. Spring4D может держать лист отсортированным при каждом добавлении элемента, и тогда метод IndexOf будет использовать более эффективный бинарный алгоритм поиска. Вот benchmark тест от автора
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
uses Spring, Spring.Collections, Winapi.Windows; var L, SL: IList<Integer>; i: Integer; Tik: Cardinal; begin L := TCollections.CreateList<Integer>; SL := TCollections.CreateSortedList<Integer>; // << Отсортированная коллекция Tik := GetTickCount; for i := 1 to 1000000 do L.Add(i); Writeln('Adding to list: ', GetTickCount - Tik); Tik := GetTickCount; for i := 1 to 1000000 do SL.Add(i); Writeln('Adding to sorted list: ', GetTickCount - Tik); Tik := GetTickCount; for i := 1 to 100000 do L.IndexOf(i); Writeln('Searching list: ', GetTickCount - Tik); Tik := GetTickCount; for i := 1 to 100000 do SL.IndexOf(i); Writeln('Searching sorted list: ', GetTickCount - Tik); end. |
И результаты автора
1 2 3 4 |
Adding to list: 78 Adding to sorted list: 94 Searching list: 17890 Searching sorted list: 31 |
У меня цифры немного другие, но суть та же.
Multimap
Это такой “словарь” с не уникальными ключами. Иногда это может быть нужно. То есть не нужно в ручную городить свою конструкцию <key, value> и помещать, например в TObjectList<TMyManualConstruction> Работает это так (источник примера)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
uses Spring, Spring.Collections; var M: IMultiMap<Integer, String>; begin M := TCollections.CreateMultiMap<Integer, String>; M.Add(1, 'One'); M.Add(1, 'First'); M.Add(1, 'Single'); M.Add(2, 'Two'); M.Add(2, 'Second'); Writeln(M.Count); // Outputs 5 Writeln(M.Items[1].Count); // Outputs 3 end. |
Также в библиотеке много других полезных структур, расширяющих стандартные возможности Delphi, например, IDictionary<K, V> и соответственно AsReadonlyDictionary<K, V>. Ну и конечно стеки, очереди
Ссылки по теме
Обзор Spring4D на блоге PascalToday
Обзор DependencyInjection от Алексея Тимохина