Недавно я писал о преимуществах работы с IEnumerable в сравнении со всякими листами и массивами, сегодня немного продолжу эту же тему.
Мысль навеяна сниппетом "invoke" из Visual Studio, который (совершенно правильно) предлагает возбуждать событие вот таким вот образом:
| public event EventHandler StateChanged; |
| private void OnStateChanged() |
| { |
| EventHandler changedHandler = StateChanged; |
| if (changedHandler != null) |
| { |
| changedHandler(this, EventArgs.Empty); |
| } |
| } |
вместо "привычного" и часто встречающегося:
| public event EventHandler StateChanged; |
| private void OnStateChanged() |
| { |
| if (StateChanged != null) |
| { |
| StateChanged(this, EventArgs.Empty); |
| } |
| } |
Вся "фишка" здесь, понятное дело, в многопоточности: во втором случае подписчик (в другом потоке) может отписаться от события как раз в тот момент, когда проверка на null уже прошла, но перед тем, как будет произведена попытка возбудить событие. Соответственно, второй случай - потенциальный кандидат на NullReferenceException, к тому же трудно воспроизводимый. В то же время в случае первого примера возникновение такого исключения невозможно.
Теперь вернемся к коллекциям и посмотрим на них с этой стороны.
Допустим, у нас есть какой-либо объект, который "выставляет наружу" коллекцию (сейчас нам не важно, какого типа, массив ли, IEnumerable ли, пусть это будет List<> для примера):
| public sealed class TestObject |
| { |
| public List<string> Users { get; set; } |
| |
| public void AddUser(string user) |
| { |
| Users.Add(user); |
| } |
| } |
Предположим, что есть некий код, работающий с экземпляром этого объекта. Допустим, что этот код зачем-то перебирает список пользователей, который содержится в объекте, с помощью foreach, такое часто бывает. А в это время, в другом потоке, приходит команда на добавление нового пользователя в список с помощью, например, метода AddUser(...). В этом случае мы немедленно получаем исключение в том коде, который занимался перебором значений. Исключение будет связано с тем, что коллекция была изменена и дальнейший перебор ее невозможен. Кстати, узнать из исключения кто, где и зачем поменял коллекцию будет невозможно, что тоже не сахар в смысле отладки.
Хорошо, будем перебирать коллекцию не с помощью foreach, а с помощью for, в этом случае от такого рода исключения мы еще как-то застрахуемся:
| for (int i=0;i<to.Users.Count;i++) |
| { |
| Console.WriteLine(to.Users[i]); |
| Thread.Sleep(500); |
| } |
Однако, опять же, ситуация ничем не лучше: представьте, что кто-то удалил все элементы из списка в тот момент, когда проверка условия цикла уже прошла и мы пытаемся получить i-й элемент массива. Опять исключение. И хорошо еще, если исключение. А если в этот момент коллекция просто изменится и i-м окажется уже совсем другой элемент, не тот, с которым мы собирались работать?
В общем, думаю, проблема понятна. Что с решением?
В общем виде решение, очевидно, нужно искать в неизменяемости значений, то, что называется Immutable object.
Immutable object не может изменить своего значения после того, как он единожды был инициализирован. Пример - тип String в .NET. К слову, в .NET нет возможности объявить объект константным (неизменяемым), к большому сожалению. Поэтому приходится "выкручиваться" на предмет immutable objects по-всякому. Об этом, возможно, в следующий раз.
Что может дать такая неизменяемость? На самом деле очень многое. Такие объекты можно без опасений использовать из нескольких потоков, не беспокоясь о том, что их состояние вдруг изменится, не переживая по поводу "грязного чтения" (это когда в одном потоке вы уже успели изменить 5 свойств объекта, а еще 15 на очереди, когда кто-то начинает работать с тем же объектом в другом потоке, получая, по сути, невалидный объект) не заморачиваясь излишней синхронизацией... На самом деле, представьте, сколько придется писать кода для синхронизации потоков, всяческих lock'ов, для того, чтобы вышеуказанный простой пример стал потокобезопасным, надежным? А ведь надо еще не запутаться в этих локах, не допустить мертвых блокировок.. А если что-то посложнее?
Получается, что для того, чтобы решить нашу задачу, мы должны соблюсти два условия:
Первое. Использовать неизменяемые коллекции. При этом возвращение из свойства "стандартной" ReadOnlyCollection<> (в виде "return m_Users.AsReadOnly();") нам, в общем случае, не подойдет, так как она не имеет своего енумератора и является всего лишь оберткой над List<>, изменив который мы получим все те же проблемы со "сломанным" енумератором. Массив нам не подойдет тоже, так как позволяет изменять элементы используя индексатор.
Получается, что придется делать собственную реализацию, скажем, ReadOnlyList<T> : IList<T>, который не позволит изменять коллекцию вообще.
Второе. Вытекает из первого. Так, как мы собираемся хранить в нашем TestObject не List<T>, а ReadOnlyList<T>, то при любой необходимости изменения коллекции мы просто создаем новый ReadOnlyList на основе имеющегося+все необходимые изменения, а потом просто подменяем им существующий. Подмена ссылки - операция атомарная, поэтому "грязного чтения" не возникнет.
Соблюдя эти два простых условия мы получим следующую простую ситуацию: те, кто уже получил ссылку на коллекцию из свойства Users, так и продолжат работать с ней, с той, которая была валидна на момент обращения к свойству, даже если само свойство Users и поменяет свое значение и станет возвращать уже другую коллекцию. И никто не умрёт :)
За исключением, однако, случая из последнего примера (цикл for). Здесь ошибки все еще возможны, так как обращение к свойству Users производится дважды (на каждую итерацию): в момент проверки условия и в момент обращения к элементу. Между двумя этими моментами Users может поменять свое значение - и "привет".
От этого последнего неудобства можно избавиться аж двумя способами:
- Тем же способом, как и в случае с событием, о котором я говорил в самом начале: сначала получить ссылку на экземпляр, а потом уже делать с ним все, что хочешь и енумерировать сколь угодно долго любыми циклами. Однако, это неудобно: не заставишь же разработчиков не использовать некоторых конструкций. Поэтому, смотрим пукнт 2.
- Возвращать IEnumerable<> :) Во-первых это и означает получение ссылки и дальнейшую работу только с ней, а во-вторых, for'ом по такому свойству пройтись просто не получится :) Если кому-то и нужно использовать именно конструкцию for, или просто нужен массив, или лист, то, опять же, создать его на основе IEnumerable<> не представляет никакой сложности. И, главное, это не скажется на работе других частей приложения.
Меньше локов, товарищи ;)