По сути, каждый экземпляр делегата поставляется со списком вызовов , который, по сути, является просто списком всех ссылок на целевые события, с которыми вы связываетесь. Например,
public event DirListEvent<string> FileRetrieved;
код выше создает тип делегата с именем FileRetrieved (как вы уже знаете).
Этот тип делегата теперь имеет список вызовов, в котором хранятся все экземпляры делегатов, которые вы зарегистрировали с помощью оператора + = или метода Delegate.Combine.
obj.FileRetrieved += printFilename;
obj.FileRetrieved += printFilename;
Этот код в основном добавляет 2 одинаковых экземпляра делегата в список вызовов FileRetrieved. Список вызовов допускает дублирование экземпляров. Следовательно, когда вызывается FileRetrieved, он просматривает свой список вызовов и обнаруживает, что добавлено 2 метода (оба printFilename), и просто делегирует вызовы printFilename, как и предполагалось .
Попробуйте использовать еще одну строку obj.FileRetrieved += printFilename;
, и FileRetrieved должен вызвать printFilename 3 раза.