ConcurrentDictionary
Представляет потокобезопасную коллекцию пар «ключ-значение», доступ к которой могут одновременно получать несколько потоков.
Для настройки есть 2 основных параметра:
сapacity — первоначальное кол-во элементов. По умолчанию — 31.
concurrencyLevel – предполагаемое число потоков на запись. По умолчанию = 4
Методы ConcurrentDictionary.
Основные Методы словаря можно разделить на 3 группы:
- полностью неблокируемые;
- блокировка одного элемента из пула блокировок;
- блокировка всего словаря;
К полностью не блокируемым операциям можно отнести:
- ContainsKey
- TryGet
- this
- GetEnumerator – операция не обеспечивает целостность данных (не использует снепшоты), т.е. данные за время работы функции могут поменяться.
Все операции чтения (Get/ContainsKey) имеют примерно одинаковый алгоритм работы:
- вычисление хеша ключа через GetHashCode()
- вычисление бакета, в котором лежит наш элемент
- сравнения значения ключа в бакете с тем, который у нас
- чтение значения с использованием Volatile.Read
К операциям с блокировкой одного элемента из пула блокировок можно отнести:
- TryAdd
- TryUpdate
- TryRemove
Ниже примерный алгоритм работы:
- Вычисление хеша ключа нового элемента
- Вычисление бакета bucketNo, в который будет добавлен элемент, и номера блокировки из пула
- Блокировка bucketNo через Monitor.Enter
- Запись элемента с использованием Volatile.Write
- Освобождение блокировки Monitor.Exit
К самым неэффективным операциям, которые блокируют весь словарь, относятся:
- Count, IsEmpty. Да, эти операции требуют полной блокировки словаря. Если вам необходимо сохранить в лог-файл число элементов, то можно использовать GetEnumerator и LINQ. Так же эти методы захватывают все локи в словаре. Лучше воздержаться от частого вызова этих свойств из нескольких потоков.
- Keys, Values – получение списка ключей и списка значений соответственно. Они не только берут все локи, но и целиком копируют в отдельный List все ключи и значения. В отличие от традиционного Dictionary, одноимённые свойства которого возвращают «тонкие» обертки, здесь нужно быть готовым к крупным аллокациям памяти.
- CopyTo – explicit ICollection
- Clear, ToArray
В отличие от обычного Dictionary, можно производить вставку в ConcurrentDictionary или удаление из него прямо во время перечисления
Удалять элементы можно не только по ключу, но и по точному совпадению key + value, причем атомарно! Это недокументированная возможность, скрытая за explicit-реализацией интерфейса ICollection. Она позволяет безопасно очищать такой кэш даже в условиях гонки с обновлением значения:
в условиях конкурентного доступа GetOrAdd может вызвать делегат-фабрику для одного ключа сильно больше одного раза. Если так делать нельзя или дорого, достаточно обернуть значение в Lazy:
Overview
One of the nice features of other scripting languages, such as Perl, LISP, and Python is what is called an associative array. A n associative array differs from a “normal” array in one major way: rather than being indexed numerically (i.e. 0, 1, 2, 3, …), it is indexed by a key, or an English-like word. VBScript has something very similar to an associative array. This object is called the Dictionary object. The VBScript Dictionary object provides an item indexing facility. Dictionaries are part of Microsoft’s Script Runtime Library.
The Dictionary object is used to hold a set of data values in the form of (key, item) pairs. A dictionary is sometimes called an associative array because it associates a key with an item. The keys behave in a way similar to indices in an array, except that array indices are numeric and keys are arbitrary strings. Each key in a single Dictionary object must be unique.
Dictionaries are frequently used when some items need to be stored and recovered by name. For example, a dictionary can hold all the environment variables defined by the system or all the values associated with a registry key. However, a dictionary can only store one item for each key value. That is, dictionary keys must all be unique.
Creating Dictionaries
To construct an instance of a dictionary object, just use the following lines of code:
Dictionary objects have one property that should be set before any data values are stored in the dictionary. There are two modes for the .CompareMode property which control how individual keys are compared. If the mode is (the default), upper and lower case letters in keys are considered distinct. If the mode is , upper and lower case letters in keys are considered identical. This means that a Dictionary object in binary mode can contain two keys “Key” and “key”, whereas these would be considered the same key in text mode.
Наполнение словаря
4.1. Типы данных ключа и элемента
Dictionary наполняется по одному элементу. Не существует способов наполнить словарь массово. Чтобы добавить в словарь новый элемент вы должны иметь уникальный ключ и сам элемент, который под этим ключом будет храниться в словаре.
В качестве типа данных для элемента может быть использовано практически всё что угодно: числа, логический тип, строки (в том числе пустые), дата-время, массивы, любые объекты (листы, диапазоны, коллекции, другие словари, пустой указатель Nothing ).
В качестве типа данных для ключа могут быть использованы: числа, строки, дата-время, объекты, но не массивы.
UDT (User Defined Type) не может напрямую использоваться в качестве ключа и/или элемента, но данное ограничение можно обойти, объявив аналог UDT , создав класс и определив в нём свойства аналогичные имеющимся в UDT . А поскольку класс — это объектный тип, то его уже можно использовать для ключей и элементов.
4.2. Через метод Add
На листе Example , прилагаемого к статье файла, есть таблица с TOP30 стран по площади их территории. Для области данных этой таблицы объявлен именованный диапазон SquareByCountry . Пример ниже добавляет все строки указанногот ИД в Dictionary по принципу страна ( key ) — площадь ( item ):
Как видите, для добавления элемента (item) мы в 12-й строке кода использовали метод Add объекта dicCountry . Если в нашей таблице будет задвоена страна, то при попытке добавить в словарь элемента с ключом, который в словаре уже есть, будет сгенерировано исключение:
4.3. Через свойство Item
Используя свойство Item , также можно добавлять пары ключ-элемент, однако, при попытке добавить дублирующий ключ исключения сгенерировано НЕ БУДЕТ , а элемент будет заменён на новый (с потерей старого). Это очень полезно — иметь возможность выбирать способы наполнения словаря, отличающиеся реакцией на задвоение ключей.
4.4. Неявное добавление ключа в Dictionary
И ещё один неожиданный и я бы сказал экзотический способ пополнения словаря. Если упомянуть свойство Item по ПРАВУЮ сторону оператора присваивания, то он оказывается добавит в словарь key с пустым item, если данного key не существует в коллекции. Если же такой key уже существует, то никаких действий предпринято не будет.
Ещё раз хочу обратить ваше внимание, что элемент (item) при таком пополнении коллекции будет пустым ( Empty ). Это можно использовать, если вам нет необходимости что-то хранить в элементах в качестве полезной нагрузки (например, когда вы просто строите список уникальных значений, встречающихся в столбце таблицы)
Если вы читаете словарь через Item (а это, собственно, самый логичный и распространенный метод), и при этом хотите избежать добавления пустых ключей в словарь, используйте предварительно метод Exists , что контроля наличия такого ключа в коллекции.
OrderedDictionary
Иногда бывают моменты когда вы хотите использовать ключи для поиска или foreach для итерации с помощью DictionaryEntry объектов. Элементы OrderedDictionary доступны с помощью ключа или индекса .Элементы OrderedDictionary не сортируются по ключу, в отличие от элементов класса который мы рассматриваем выше.
Вы можете видеть, что я использовал два разных метода, чтобы перебрать коллекцию. В первом примере я использовал цикл for и числовой индекс для извлечения каждого объекта (в данном случае строки), хранящегося в коллекции.
Другим преимуществом является скорость. При просмотре большой коллекции чтение OrderedDictionary с использованием первого примера, числового индекса, всегда будет быстрее, чем при использовании метода стиля словаря.
Когда вам нужна «мощь» коллекции и простой доступ к числовому индексу, OrderedDictionary является предпочтительной коллекцией.
Performance: Dictionary vs. Collection
ceteris paribus
In an Excel worksheet, I made 50 columns, each with 10,000 random integers. The ranges within which these numbers varied was different in each column.
I then wrote a VBA procedure that identified the distinct values in each column, and wrote those distinct values underneath the analyzed values.
This procedure used a Collection approach first to identify and then write the distinct values, and then three different Dictionary-based approaches to doing the same. Each approach was run 20 times in succession.
For all four of these approaches, my procedure captured the start time and end time, and thus the time required for the operation.
The procedure then wrote the results of each trial to a worksheet for comparison and analysis.
To test the impact of early binding vs. late binding, I made two copies of the workbook, using the same test data and the same test code (differing only in how I declared and instantiated my Dictionary variable).
Benchmark-Early-Binding.xlsm
Benchmark-Late-Binding.xlsm
Adding Items to the Dictionary
Function | Params | Example |
---|---|---|
Add | Key, Item | dict.Add «Apples», 50 |
We can add items to the dictionary using the Add function. Items can also be added by assigning a value which we will look at in the next section.
Let’s look at the Add function first. The Add function has two parameters: Key and Item. Both must be supplied
dict.Add Key:="Orange", Item:=45 dict.Add "Apple", 66 dict.Add "12/12/2015", "John" dict.Add 1, 45.56
In the first add example above we use the parameter names. You don’t have to do this although it can be helpful when you are starting out.
The Key can be any data type. The Item can be any data type, an object, array, collection or even a dictionary. So you could have a Dictionary of Dictionaries, Array and Collections. But most of the time it will be a value(date, number or text).
If we add a Key that already exists in the Dictionary then we will get the error
The following code will give this error
dict.Add Key:="Orange", Item:=45 ' This line gives an error as key exists already dict.Add Key:="Orange", Item:=75
Other useful functions
Function | Parameters | Example |
---|---|---|
Count | N/A | dict.Count |
Remove | Key | dict.Remove «Apples» |
RemoveAll | N/A | dict.RemoveAll |
The three functions in the above table do the following:
- Count – returns the number of items in the Dictionary.
- Remove – removes a given key from the Dictionary.
- RemoveAll – removes all items from the Dictionary
The following sub shows an example of how you would use these functions
' https://excelmacromastery.com/ Sub AddRemoveCount() Dim dict As New Scripting.Dictionary ' Add some items dict.Add "Orange", 55 dict.Add "Peach", 55 dict.Add "Plum", 55 Debug.Print "The number of items is " & dict.Count ' Remove one item dict.Remove "Orange" Debug.Print "The number of items is " & dict.Count ' Remove all items dict.RemoveAll Debug.Print "The number of items is " & dict.Count End Sub
Remember that you can download all the code examples from the post. Just go to the at the top.
Методы и свойства словаря
Методы объекта Dictionary
Метод Add
Метод Add добавляет в словарь новую пару ключ–элемент.
1 | Dictionary.AddКлюч,Элемент |
- Ключ – обязательный аргумент, представляющий ключ добавляемой пары.
- Элемент – обязательный аргумент, представляющий элемент добавляемой пары.
Если добавляемый ключ в словаре уже есть, VBA Excel сгенерирует ошибку.
Метод Exists
Метод Exists возвращает логическое значение, указывающее, существует ли в словаре указанный ключ. True – указанный ключ существует, False – указанный ключ не существует.
1 | Dictionary.Exists(Ключ) |
Метод Items
Метод Items возвращает массив всех элементов в словаре.
1 | Dictionary.Items |
Метод Keys
Метод Keys возвращает массив всех ключей в словаре.
1 | Dictionary.Keys |
Метод Remove
Метод Remove удаляет из словаря одну пару ключ–элемент.
1 | Dictionary.Remove(Ключ) |
Если указанный ключ не существует, произойдет ошибка.
Метод RemoveAll
Метод RemoveAll удаляет из словаря все пары ключей и элементов.
1 | Dictionary.RemoveAll |
Свойства объекта Dictionary
Свойство CompareMode
Свойство CompareMode задает или возвращает режим сравнения ключей в словаре. Используется для чтения и записи.
1 | Dictionary.CompareModeСравнение |
Необязательный аргумент Сравнение используется только при записи и может принимать следующие значения:
- –1 (vbUseCompareOption) – выполняется сравнение, заданное оператором Option Compare.
- (vbBinaryCompare) – выполняется двоичное сравнение.
- 1 (vbTextCompare) – выполняется текстовое сравнение.
По умолчанию выполняется двоичное сравнение, при котором значение свойства CompareMode равно (vbBinaryCompare).
Свойство Count
Свойство Count возвращает количество элементов в словаре. Только для чтения.
1 | Dictionary.Count |
Свойство Item
Свойство Item задает или возвращает элемент для указанного ключа в словаре. Используется для чтения и записи.
1 | Dictionary.Item(Ключ)=Элемент |
- Ключ – обязательный аргумент, представляющий из себя существующий ключ для чтения или изменения существующего элемента, или новый ключ для создания новой пары ключ–элемент.
- Элемент – необязательный аргумент, представляющий выражение, которое заменяет существующий элемент с существующим ключом или добавляет новый элемент в пару ключ–элемент с новым ключом.
Если при создании с новым ключом новой пары ключ–элемент аргумент Элемент не указан, значение элемента будет пустым (Empty).
Свойство Key
Свойство Key задает новое значение ключа для существующего ключа в словаре.
1 | Dictionary.Key(Ключ)=НовыйКлюч |
- Ключ – обязательный аргумент, представляющий из себя существующий ключ.
- НовыйКлюч – обязательный аргумент, представляющий выражение, значение которого заменяет существующий ключ.
Если указанный ключ отсутствует в словаре, произойдет ошибка.
Sorting the Dictionary
Sometimes you may wish to sort the Dictionary either by key or by value.
The Dictionary doesn’t have a sort function so you have to create your own. I have written two sort functions – one for sorting by key and one for sorting by value.
Sorting by keys
To sort the dictionary by the key you can use the SortDictionaryByKey function below
' https://excelmacromastery.com/ Public Function SortDictionaryByKey(dict As Object _ , Optional sortorder As XlSortOrder = xlAscending) As Object Dim arrList As Object Set arrList = CreateObject("System.Collections.ArrayList") ' Put keys in an ArrayList Dim key As Variant, coll As New Collection For Each key In dict arrList.Add key Next key ' Sort the keys arrList.Sort ' For descending order, reverse If sortorder = xlDescending Then arrList.Reverse End If ' Create new dictionary Dim dictNew As Object Set dictNew = CreateObject("Scripting.Dictionary") ' Read through the sorted keys and add to new dictionary For Each key In arrList dictNew.Add key, dict(key) Next key ' Clean up Set arrList = Nothing Set dict = Nothing ' Return the new dictionary Set SortDictionaryByKey = dictNew End Function
The code below, shows you how to use SortDictionaryByKey
' https://excelmacromastery.com/ Sub TestSortByKey() Dim dict As Object Set dict = CreateObject("Scripting.Dictionary") dict.Add "Plum", 99 dict.Add "Apple", 987 dict.Add "Pear", 234 dict.Add "Banana", 560 dict.Add "Orange", 34 PrintDictionary "Original", dict ' Sort Ascending Set dict = SortDictionaryByKey(dict) PrintDictionary "Key Ascending", dict ' Sort Descending Set dict = SortDictionaryByKey(dict, xlDescending) PrintDictionary "Key Descending", dict End Sub Public Sub PrintDictionary(ByVal sText As String, dict As Object) Debug.Print vbCrLf & sText & vbCrLf & String(Len(sText), "=") Dim key As Variant For Each key In dict.keys Debug.Print key, dict(key) Next End Sub
Sorting by values
To sort the dictionary by the values you can use the SortDictionaryByValue function below.
' https://excelmacromastery.com/ Public Function SortDictionaryByValue(dict As Object _ , Optional sortorder As XlSortOrder = xlAscending) As Object On Error GoTo eh Dim arrayList As Object Set arrayList = CreateObject("System.Collections.ArrayList") Dim dictTemp As Object Set dictTemp = CreateObject("Scripting.Dictionary") ' Put values in ArrayList and sort ' Store values in tempDict with their keys as a collection Dim key As Variant, value As Variant, coll As Collection For Each key In dict value = dict(key) ' if the value doesn't exist in dict then add If dictTemp.exists(value) = False Then ' create collection to hold keys ' - needed for duplicate values Set coll = New Collection dictTemp.Add value, coll ' Add the value arrayList.Add value End If ' Add the current key to the collection dictTemp(value).Add key Next key ' Sort the value arrayList.Sort ' Reverse if descending If sortorder = xlDescending Then arrayList.Reverse End If dict.RemoveAll ' Read through the ArrayList and add the values and corresponding ' keys from the dictTemp Dim item As Variant For Each value In arrayList Set coll = dictTemp(value) For Each item In coll dict.Add item, value Next item Next value Set arrayList = Nothing ' Return the new dictionary Set SortDictionaryByValue = dict Done: Exit Function eh: If Err.Number = 450 Then Err.Raise vbObjectError + 100, "SortDictionaryByValue" _ , "Cannot sort the dictionary if the value is an object" End If End Function
The code below shows you how to use SortDictionaryByValue
' https://excelmacromastery.com/ Sub TestSortByValue() Dim dict As Object Set dict = CreateObject("Scripting.Dictionary") dict.Add "Plum", 99 dict.Add "Apple", 987 dict.Add "Pear", 234 dict.Add "Banana", 560 dict.Add "Orange", 34 PrintDictionary "Original", dict ' Sort Ascending Set dict = SortDictionaryByValue(dict) PrintDictionary "Value Ascending", dict ' Sort Descending Set dict = SortDictionaryByValue(dict, xlDescending) PrintDictionary "Value Descending", dict End Sub Public Sub PrintDictionary(ByVal sText As String, dict As Object) Debug.Print vbCrLf & sText & vbCrLf & String(Len(sText), "=") Dim key As Variant For Each key In dict.keys Debug.Print key, dict(key) Next key End Sub
The Key and Case Sensitivity
Some of the string functions in VBA have a . This is used for functions that compare strings. It is used to determine if the case of the letters matter.
BigStockPhoto.com
The Dictionary uses a similar method. The CompareMode property of the Dictionary is used to determine if the case of the key matters. The settings are
vbTextCompare: Upper and lower case are considered the same.
vbBinaryCompare: Upper and lower case are considered different. This is the default.
With the Dictionary we can use these settings to determine if the case of the key matters.
' https://excelmacromastery.com/ Sub CaseMatters() Dim dict As New Scripting.Dictionary dict.CompareMode = vbBinaryCompare dict.Add "Orange", 1 ' Prints False because it considers Orange and ORANGE different Debug.Print dict.Exists("ORANGE") Set dict = Nothing End Sub
This time we use vbTextCompare which means that the case does not matter
' https://excelmacromastery.com/ Sub CaseMattersNot() Dim dict As New Scripting.Dictionary dict.CompareMode = vbTextCompare dict.Add "Orange", 1 ' Prints true because it considers Orange and ORANGE the same Debug.Print dict.Exists("ORANGE") Set dict = Nothing End Sub
Note: The Dictionary must be empty when you use the CompareMode property or you will get the error: “Invalid procedure call or argument”.
Things to Watch Out For
vbBinaryCompare (the case matters) is the default and this can lead to subtle errors. For example, imagine you have the following data in cells A1 to B2.
Orange, 5
orange, 12
The following code will create two keys – on for “Orange” and one for “orange”. This is subtle as the only difference is the case of the first letter.
' https://excelmacromastery.com/ Sub DiffCase() Dim dict As New Scripting.Dictionary dict.Add Key:=(Range("A1")), Item:=Range("B1") dict.Add Key:=(Range("A2")), Item:=Range("B2") End Sub
If you do use vbTextCompare for the same data you will get an error when you try to add the second key as it considers “Orange” and “orange” the same.
' https://excelmacromastery.com/ Sub UseTextcompare() Dim dict As New Scripting.Dictionary dict.CompareMode = vbTextCompare dict.Add Key:=(Range("A1")), Item:=Range("B1") ' This line will give an error as your are trying to add the same key dict.Add Key:=(Range("A2")), Item:=Range("B2") End Sub
If you use the assign method then it does not take the CompareMode into account. So the following code will still add two keys even though the CompareMode is set to vbTextCompare.
' https://excelmacromastery.com/ Sub Assign() Dim dict As New Scripting.Dictionary dict.CompareMode = vbTextCompare ' Adds two keys dict(Range("A1")) = Range("B1") dict(Range("A2")) = Range("B2") ' Prints 2 Debug.Print dict.Count End Sub
Available Options
- VB6/VBA Collection object. Provided with the programming language this class allows adding items with or without key and retrieving them by key or by index. Keys can only be of datatype and key matching is always case-insensitive. Collection supports iteration over its items with the syntax and is optimized for fast adding and retrieving items by key. It can be created like this: . This object does not exist in VBScript.
- Scripting.Dictionary comes with the «Microsoft Scripting Runtime» COM library (scrrun.dll) and is a very fast key-value-store that supports iteration over its items with the syntax. Every item must have a key. Keys can be of any datatype or they can be objects. Number keys are data-type-insensitive which means that the key as = as = as . String key matching can be either case-sensitive or case-insensitive. If the library is not referenced in your project (F2 > Right Mouse Click > References), you have to use the syntax to create a new Dictionary object. Scripting.Dictionary does not exist on Apple Mac environments.
- DictCollection is implemented in VB6/VBA and supports adding items with or without keys. It has two emulation modes and that mimic the behavior of Dictionary and Collection. Iteration using the syntax is not supported (not possible with VB6/VBA classes). Keys have to be Strings and key matching can be case-sensitive or case-insensitive. Items and keys are stored internally as arrays so retrieving items by index is very fast. DictCollection has extended functionality like , , , , , , , , , and , and comes with useful String and Array functions like , , and . Keys can also be searched and filtered using wildcards like using the , functions.
A Quick Guide to the VBA Dictionary
Function | Example |
---|---|
Early binding reference | “Microsoft Scripting Runtime” (Add using Tools->References from the VB menu) |
Declare (early binding) | Dim dict As Scripting.Dictionary |
Create(early binding) | Set dict = New Scripting.Dictionary |
Declare (late binding) | Dim dict As Object |
Create(late binding) | Set dict = CreateObject(«Scripting.Dictionary») |
Add item (key must not already exist) | dict.Add Key, Value e.g. dict.Add «Apples», 50 |
Change value at key. Automatically adds if the key does not exist. | dict(Key) = Value e.g. dict(«Oranges») = 60 |
Get a value from the dictionary using the key | Value = dict(Key) e.g. appleCount = dict(«Apples») |
Check if key exists | dict.Exists(Key)e.g. If dict.Exists(«Apples») Then |
Remove item | dict.Remove Key e.g. dict.Remove «Apples» |
Remove all items | dict.RemoveAll |
Go through all items (for each loop) | Dim key As Variant For Each key In dict.Keys Debug.Print key, dict(key)Next key |
Go through all items (for loop — early binding only) | Dim i As Long For i = 0 To dict.Count — 1 Debug.Print dict.Keys(i), dict.Items(i)Next i |
Go through all items (for loop — early and late binding) | Dim i As LongFor i = 0 To dict.Count — 1Debug.Print dict.Keys()(i), dict.Items()(i)Next i |
Get the number of items | dict.Count |
Make key case sensitive (the dictionary must be empty). | dict.CompareMode = vbBinaryCompare |
Make key non case sensitive (the dictionary must be empty). | dict.CompareMode = vbTextCompare |