Existe um dicionário genérico somente leitura disponível no .NET?

186

Estou retornando uma referência a um dicionário na minha propriedade somente leitura. Como evito que os consumidores alterem meus dados? Se fosse esse, IListeu poderia simplesmente devolvê-lo AsReadOnly. Existe algo semelhante que eu possa fazer com um dicionário?

Private _mydictionary As Dictionary(Of String, String)
Public ReadOnly Property MyDictionary() As Dictionary(Of String, String)
    Get
        Return _mydictionary
    End Get
End Property
Rob Sobers
fonte
4
Deve haver alguma maneira de fazê-lo, caso contrário, não haveria uma propriedade IsReadOnly no IDictionary ... ( msdn.microsoft.com/en-us/library/bb338949.aspx )
Powerlord
2
Muitos dos benefícios conceituais da imutabilidade podem ser obtidos sem a imposição do tempo de execução. Se este for um projeto privado, considere um método disciplinado e informal. Se você precisar fornecer dados a um consumidor, considere seriamente cópias profundas. Quando você considera que uma coleção imutável requer 1) referência imutável à coleção 2) impedindo a mutação da sequência em si e 3) impedindo a modificação das propriedades nos itens da coleção, e que algumas delas podem ser violadas pela reflexão, a execução do tempo de execução é não é prático.
Sprague
27
Desde o .NET 4.5, existe um System.Collections.ObjectModel.ReadOnlyDictionary ^ _ ^
Smartkid
2
Agora também há coleções imutáveis ​​da Microsoft via NuGet msdn.microsoft.com/en-us/library/dn385366%28v=vs.110%29.aspx
VoteCoffee

Respostas:

156

Aqui está uma implementação simples que envolve um dicionário:

public class ReadOnlyDictionary<TKey, TValue> : IDictionary<TKey, TValue>
{
    private readonly IDictionary<TKey, TValue> _dictionary;

    public ReadOnlyDictionary()
    {
        _dictionary = new Dictionary<TKey, TValue>();
    }

    public ReadOnlyDictionary(IDictionary<TKey, TValue> dictionary)
    {
        _dictionary = dictionary;
    }

    #region IDictionary<TKey,TValue> Members

    void IDictionary<TKey, TValue>.Add(TKey key, TValue value)
    {
        throw ReadOnlyException();
    }

    public bool ContainsKey(TKey key)
    {
        return _dictionary.ContainsKey(key);
    }

    public ICollection<TKey> Keys
    {
        get { return _dictionary.Keys; }
    }

    bool IDictionary<TKey, TValue>.Remove(TKey key)
    {
        throw ReadOnlyException();
    }

    public bool TryGetValue(TKey key, out TValue value)
    {
        return _dictionary.TryGetValue(key, out value);
    }

    public ICollection<TValue> Values
    {
        get { return _dictionary.Values; }
    }

    public TValue this[TKey key]
    {
        get
        {
            return _dictionary[key];
        }
    }

    TValue IDictionary<TKey, TValue>.this[TKey key]
    {
        get
        {
            return this[key];
        }
        set
        {
            throw ReadOnlyException();
        }
    }

    #endregion

    #region ICollection<KeyValuePair<TKey,TValue>> Members

    void ICollection<KeyValuePair<TKey, TValue>>.Add(KeyValuePair<TKey, TValue> item)
    {
        throw ReadOnlyException();
    }

    void ICollection<KeyValuePair<TKey, TValue>>.Clear()
    {
        throw ReadOnlyException();
    }

    public bool Contains(KeyValuePair<TKey, TValue> item)
    {
        return _dictionary.Contains(item);
    }

    public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
    {
        _dictionary.CopyTo(array, arrayIndex);
    }

    public int Count
    {
        get { return _dictionary.Count; }
    }

    public bool IsReadOnly
    {
        get { return true; }
    }

    bool ICollection<KeyValuePair<TKey, TValue>>.Remove(KeyValuePair<TKey, TValue> item)
    {
        throw ReadOnlyException();
    }

    #endregion

    #region IEnumerable<KeyValuePair<TKey,TValue>> Members

    public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
    {
        return _dictionary.GetEnumerator();
    }

    #endregion

    #region IEnumerable Members

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    #endregion

    private static Exception ReadOnlyException()
    {
        return new NotSupportedException("This dictionary is read-only");
    }
}
Thomas Levesque
fonte
11
+1 por postar código completo e não apenas um link, mas estou curioso, qual é o sentido de um construtor vazio em um ReadOnlyDictionary? :-)
Samuel Neff
20
Cuidado com esse construtor. Se você fizer uma cópia de referência do dicionário transmitido, é possível que um código externo modifique seu dicionário "Somente leitura". Seu construtor deve fazer uma cópia completa e aprofundada do argumento.
askheaves
25
@askheaves: Boa observação, mas muitas vezes é útil usar a referência original nos tipos Somente Leitura - mantenha em sua variável privada o original e modifique-o para consumidores externos. Por exemplo, confira os objetos ReadOnlyObservableCollection ou ReadOnlyCollection que estão embutidos: Thomas forneceu algo que funciona exatamente como os inerentes à estrutura .Net. Obrigado Thomas! 1
Matt DeKrey
13
@ user420667: da maneira como é implementada, é uma "exibição somente leitura de um dicionário não somente leitura". Algum outro código pode alterar o conteúdo do dicionário original e essas alterações serão refletidas no dicionário somente leitura. Poderia ser o comportamento desejado, ou não, dependendo do que você deseja alcançar ...
Thomas Levesque
6
@ Thomas: Isso é o mesmo que um ReadOnlyCollection no .NET BCL. É uma exibição somente leitura em uma coleção possivelmente mutável. ReadOnly não significa imutável e a imutabilidade não deve ser esperada.
Jeff Yates
229

.NET 4.5

Introduz o .NET Framework 4.5 BCL ReadOnlyDictionary<TKey, TValue>( origem ).

Como o BCL do .NET Framework 4.5 não inclui um AsReadOnlypara dicionários, você precisará escrever seus próprios (se desejar). Seria algo como o seguinte, cuja simplicidade talvez destaca por que não era uma prioridade para o .NET 4.5.

public static ReadOnlyDictionary<TKey, TValue> AsReadOnly<TKey, TValue>(
    this IDictionary<TKey, TValue> dictionary)
{
    return new ReadOnlyDictionary<TKey, TValue>(dictionary);
}

.NET 4.0 e abaixo

Antes do .NET 4.5, não havia nenhuma classe de estrutura .NET que envolva um exemplo Dictionary<TKey, TValue>como o ReadOnlyCollection agrupa uma lista . No entanto, não é difícil criar um.

Aqui está um exemplo - existem muitos outros se você pesquisar no Google no ReadOnlyDictionary .

Jeff Yates
fonte
7
Não parece que eles se lembraram de fazer um AsReadOnly()método habitual Dictionary<,>, então eu imagino quantas pessoas descobrirão seu novo tipo. Esse encadeamento de estouro de pilha ajudará, no entanto.
Jeppe Stig Nielsen
@Eppe: Duvido que tenha alguma coisa a ver com lembrar. Cada recurso custa e eu duvido que o AsReadOnly tenha sido o principal na lista de prioridades, especialmente porque é tão fácil de escrever.
Jeff Yates
1
Observe que isso é simplesmente um invólucro; alterações no dicionário subjacente (o que foi passado ao construtor) ainda sofrerão alterações no dicionário somente leitura. Veja também stackoverflow.com/questions/139592/…
TrueWill
1
@JeffYates Considerando como é simples, escrever levaria menos tempo do que decidir se deveria ou não passar tempo escrevendo. Por isso, minha aposta é em "eles esqueceram".
Dan Bechard
Como o TrueWill afirmou, o dicionário subjacente ainda pode ser alterado. Você pode querer considerar a passagem de um clone do dicionário original para o construtor se você quiser verdadeiro imutabilidade (assumindo chave e tipo de valor também são imutáveis.)
user420667
19

Foi anunciado na recente conferência BUILD que desde o .NET 4.5, a interface System.Collections.Generic.IReadOnlyDictionary<TKey,TValue>está incluída. A prova está aqui (Mono) e aqui (Microsoft);)

Não tenho certeza se também ReadOnlyDictionaryestá incluído, mas pelo menos com a interface, não deve ser difícil criar agora uma implementação que exponha uma interface genérica oficial do .NET :)

knocte
fonte
5
ReadOnlyDictionary<TKey, TValue>(.Net 4.5) - msdn.microsoft.com/en-us/library/gg712875.aspx
myermian
18

Sinta-se livre para usar meu invólucro simples. Ele NÃO implementa o IDictionary, portanto, não precisa lançar exceções em tempo de execução para métodos de dicionário que alterariam o dicionário. Os métodos de mudança simplesmente não estão lá. Eu criei minha própria interface chamada IReadOnlyDictionary.

public interface IReadOnlyDictionary<TKey, TValue> : IEnumerable
{
    bool ContainsKey(TKey key);
    ICollection<TKey> Keys { get; }
    ICollection<TValue> Values { get; }
    int Count { get; }
    bool TryGetValue(TKey key, out TValue value);
    TValue this[TKey key] { get; }
    bool Contains(KeyValuePair<TKey, TValue> item);
    void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex);
    IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator();
}

public class ReadOnlyDictionary<TKey, TValue> : IReadOnlyDictionary<TKey, TValue>
{
    readonly IDictionary<TKey, TValue> _dictionary;
    public ReadOnlyDictionary(IDictionary<TKey, TValue> dictionary)
    {
        _dictionary = dictionary;
    }
    public bool ContainsKey(TKey key) { return _dictionary.ContainsKey(key); }
    public ICollection<TKey> Keys { get { return _dictionary.Keys; } }
    public bool TryGetValue(TKey key, out TValue value) { return _dictionary.TryGetValue(key, out value); }
    public ICollection<TValue> Values { get { return _dictionary.Values; } }
    public TValue this[TKey key] { get { return _dictionary[key]; } }
    public bool Contains(KeyValuePair<TKey, TValue> item) { return _dictionary.Contains(item); }
    public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex) { _dictionary.CopyTo(array, arrayIndex); }
    public int Count { get { return _dictionary.Count; } }
    public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator() { return _dictionary.GetEnumerator(); }
    IEnumerator IEnumerable.GetEnumerator() { return _dictionary.GetEnumerator(); }
}
Dale Barnard
fonte
4
+1 por não violar o IDictionarycontrato. Eu acho que é mais correto do ponto de vista de OOP para IDictionaryherdar IReadOnlyDictionary.
Sam
@ Sam concordou, e se pudéssemos voltar, acho que seria o melhor e mais correto ter IDictionary(para atual IReadOnlyDictionary) e IMutableDictionary(para atual IDictionary).
21415 MasterMastic
1
@MasterMastic Essa é uma proposta bizarra. Não me lembro de nenhuma outra classe interna que se baseia na suposição inversa de que uma coleção imutável é o que um usuário espera por padrão.
Dan Bechard
11

IsReadOnly on IDictionary<TKey,TValue>é herdado de ICollection<T>( IDictionary<TKey,TValue>estende ICollection<T>como ICollection<KeyValuePair<TKey,TValue>>). Ele não é usado ou implementado de forma alguma (e de fato é "oculto" pelo uso explícito da implementação dos ICollection<T>membros associados ).

Existem pelo menos três maneiras de solucionar o problema:

  1. Implemente um somente leitura personalizado IDictionary<TKey, TValue>e agrupe / delegue para um dicionário interno, conforme sugerido
  2. Retorne um ICollection<KeyValuePair<TKey, TValue>>conjunto como somente leitura ou IEnumerable<KeyValuePair<TKey, TValue>>dependendo do uso do valor
  3. Clone o dicionário usando o construtor copy .ctor(IDictionary<TKey, TValue>)e retorne uma cópia - dessa forma, o usuário estará livre para fazer o que quiser e não terá impacto no estado do objeto que hospeda o dicionário de origem. Observe que se o dicionário que você está clonando contiver tipos de referência (não cadeias de caracteres, como mostrado no exemplo), será necessário fazer a cópia "manualmente" e clonar os tipos de referência também.

Como um aparte; ao expor coleções, tente expor a menor interface possível - no caso de exemplo, deve ser IDictionary, pois isso permite variar a implementação subjacente sem violar o contrato público que o tipo expõe.

Neal
fonte
8

Um dicionário somente leitura pode, até certo ponto, ser substituído por Func<TKey, TValue>- eu normalmente uso isso em uma API se eu quero apenas pessoas que realizam pesquisas; é simples e, em particular, é simples substituir o back-end, se você desejar. No entanto, não fornece a lista de chaves; se isso importa depende do que você está fazendo.

Eamon Nerbonne
fonte
4

Não, mas seria fácil criar o seu próprio. IDictionary define uma propriedade IsReadOnly. Basta quebrar um dicionário e lançar uma NotSupportedException dos métodos apropriados.

wekempf
fonte
3

Nenhum disponível na BCL. No entanto, publiquei um ReadOnlyDictionary (chamado ImmutableMap) no meu projeto BCL Extras

Além de ser um dicionário totalmente imutável, ele suporta a produção de um objeto proxy que implementa o IDictionary e pode ser usado em qualquer lugar em que o IDictionary seja utilizado. Ele emitirá uma exceção sempre que uma das APIs mutantes for chamada

void Example() { 
  var map = ImmutableMap.Create<int,string>();
  map = map.Add(42,"foobar");
  IDictionary<int,string> dictionary = CollectionUtility.ToIDictionary(map);
}
JaredPar
fonte
9
Seu ImmutableMap é implementado como uma árvore equilibrada. Como no .NET, as pessoas geralmente esperam que um "dicionário" seja implementado via hash - e exibam as propriedades de complexidade correspondentes - você pode ter cuidado ao promover o ImmutableMap como um "dicionário".
perfil completo de Glenn Slayden
parece que os sites code.msdn.com estão desativados. BCLextras agora aqui github.com/scottwis/tiny/tree/master/third-party/BclExtras
BozoJoe
1

Você pode criar uma classe que implemente apenas uma implementação parcial do dicionário e oculte todas as funções de adicionar / remover / definir.

Use um dicionário internamente para o qual a classe externa passe todas as solicitações.

No entanto, como seu dicionário provavelmente contém tipos de referência, não há como impedir o usuário de definir valores nas classes mantidas pelo dicionário (a menos que essas classes sejam apenas de leitura)

Jason Coyne
fonte
1

Eu não acho que exista uma maneira fácil de fazer isso ... se o seu dicionário fizer parte de uma classe personalizada, você poderá obtê-lo com um indexador:

public class MyClass
{
  private Dictionary<string, string> _myDictionary;

  public string this[string index]
  {
    get { return _myDictionary[index]; }
  }
}
Jonas
fonte
Eu preciso ser capaz de expor o dicionário inteiro, bem como um indexador.
Rob Sobers
Esta parece ser uma solução muito boa. No entanto, os clientes da classe MyClass podem precisar saber mais sobre o dicionário, por exemplo, para iteração por meio dele. E se uma chave não existir (expor TryGetValue () de alguma forma pode ser uma boa idéia)? Você pode tornar sua resposta e código de exemplo mais completos?
Peter Mortensen
1

+1 Ótimo trabalho, Thomas. Levei o ReadOnlyDictionary um passo adiante.

Muito parecido com a solução da Dale, eu queria remover Add(), Clear(), Remove(), etc de IntelliSense. Mas eu queria que meus objetos derivados fossem implementados IDictionary<TKey, TValue>.

Além disso, gostaria que o seguinte código fosse quebrado: (Novamente, a solução de Dale também faz isso)

ReadOnlyDictionary<int, int> test = new ReadOnlyDictionary<int,int>(new Dictionary<int, int> { { 1, 1} });
test.Add(2, 1);  //CS1061

A linha Add () resulta em:

error CS1061: 'System.Collections.Generic.ReadOnlyDictionary<int,int>' does not contain a definition for 'Add' and no extension method 'Add' accepting a first argument 

O chamador ainda pode convertê-lo IDictionary<TKey, TValue>, mas o valor NotSupportedExceptionserá aumentado se você tentar usar os membros não somente leitura (da solução de Thomas).

Enfim, aqui está a minha solução para quem também queria isso:

namespace System.Collections.Generic
{
    public class ReadOnlyDictionary<TKey, TValue> : IDictionary<TKey, TValue>
    {
        const string READ_ONLY_ERROR_MESSAGE = "This dictionary is read-only";

        protected IDictionary<TKey, TValue> _Dictionary;

        public ReadOnlyDictionary()
        {
            _Dictionary = new Dictionary<TKey, TValue>();
        }

        public ReadOnlyDictionary(IDictionary<TKey, TValue> dictionary)
        {
            _Dictionary = dictionary;
        }

        public bool ContainsKey(TKey key)
        {
            return _Dictionary.ContainsKey(key);
        }

        public ICollection<TKey> Keys
        {
            get { return _Dictionary.Keys; }
        }

        public bool TryGetValue(TKey key, out TValue value)
        {
            return _Dictionary.TryGetValue(key, out value);
        }

        public ICollection<TValue> Values
        {
            get { return _Dictionary.Values; }
        }

        public TValue this[TKey key]
        {
            get { return _Dictionary[key]; }
            set { throw new NotSupportedException(READ_ONLY_ERROR_MESSAGE); }
        }

        public bool Contains(KeyValuePair<TKey, TValue> item)
        {
            return _Dictionary.Contains(item);
        }

        public void CopyTo(KeyValuePair<TKey, TValue>[] array, int arrayIndex)
        {
            _Dictionary.CopyTo(array, arrayIndex);
        }

        public int Count
        {
            get { return _Dictionary.Count; }
        }

        public bool IsReadOnly
        {
            get { return true; }
        }

        public IEnumerator<KeyValuePair<TKey, TValue>> GetEnumerator()
        {
            return _Dictionary.GetEnumerator();
        }

        IEnumerator IEnumerable.GetEnumerator()
        {
            return (_Dictionary as IEnumerable).GetEnumerator();
        }

        void IDictionary<TKey, TValue>.Add(TKey key, TValue value)
        {
            throw new NotSupportedException(READ_ONLY_ERROR_MESSAGE);
        }

        bool IDictionary<TKey, TValue>.Remove(TKey key)
        {
            throw new NotSupportedException(READ_ONLY_ERROR_MESSAGE);
        }

        void ICollection<KeyValuePair<TKey, TValue>>.Add(KeyValuePair<TKey, TValue> item)
        {
            throw new NotSupportedException(READ_ONLY_ERROR_MESSAGE);
        }

        void ICollection<KeyValuePair<TKey, TValue>>.Clear()
        {
            throw new NotSupportedException(READ_ONLY_ERROR_MESSAGE);
        }

        bool ICollection<KeyValuePair<TKey, TValue>>.Remove(KeyValuePair<TKey, TValue> item)
        {
            throw new NotSupportedException(READ_ONLY_ERROR_MESSAGE);
        }
    }
}
Robert H.
fonte
0
public IEnumerable<KeyValuePair<string, string>> MyDictionary()
{
    foreach(KeyValuePair<string, string> item in _mydictionary)
        yield return item;
}
shahkalpesh
fonte
2
Ou você pode fazer:public IEnumerable<KeyValuePair<string, string>> MyDictionary() { return _mydictionary; }
Pat
0

Esta é uma solução ruim, veja abaixo.

Para aqueles que ainda usam o .NET 4.0 ou anterior, tenho uma classe que funciona exatamente como a da resposta aceita, mas é muito mais curta. Ele estende o objeto Dicionário existente, substituindo (na verdade ocultando) determinados membros para que eles gerem uma exceção quando chamados.

Se o chamador tentar chamar Adicionar, Remover ou alguma outra operação de mutação que o Dicionário interno possui, o compilador lançará um erro. Eu uso os atributos obsoletos para gerar esses erros do compilador. Dessa maneira, você pode substituir um Dicionário por este ReadOnlyDictionary e ver imediatamente onde estão os problemas sem precisar executar o aplicativo e aguardar exceções em tempo de execução.

Dê uma olhada:

public class ReadOnlyException : Exception
{
}

public class ReadOnlyDictionary<TKey, TValue> : Dictionary<TKey, TValue>
{
    public ReadOnlyDictionary(IDictionary<TKey, TValue> dictionary)
        : base(dictionary) { }

    public ReadOnlyDictionary(IDictionary<TKey, TValue> dictionary, IEqualityComparer<TKey> comparer)
        : base(dictionary, comparer) { }

    //The following four constructors don't make sense for a read-only dictionary

    [Obsolete("Not Supported for ReadOnlyDictionaries", true)]
    public ReadOnlyDictionary() { throw new ReadOnlyException(); }

    [Obsolete("Not Supported for ReadOnlyDictionaries", true)]
    public ReadOnlyDictionary(IEqualityComparer<TKey> comparer) { throw new ReadOnlyException(); }

    [Obsolete("Not Supported for ReadOnlyDictionaries", true)]
    public ReadOnlyDictionary(int capacity) { throw new ReadOnlyException(); }

    [Obsolete("Not Supported for ReadOnlyDictionaries", true)]
    public ReadOnlyDictionary(int capacity, IEqualityComparer<TKey> comparer) { throw new ReadOnlyException(); }


    //Use hiding to override the behavior of the following four members
    public new TValue this[TKey key]
    {
        get { return base[key]; }
        //The lack of a set accessor hides the Dictionary.this[] setter
    }

    [Obsolete("Not Supported for ReadOnlyDictionaries", true)]
    public new void Add(TKey key, TValue value) { throw new ReadOnlyException(); }

    [Obsolete("Not Supported for ReadOnlyDictionaries", true)]
    public new void Clear() { throw new ReadOnlyException(); }

    [Obsolete("Not Supported for ReadOnlyDictionaries", true)]
    public new bool Remove(TKey key) { throw new ReadOnlyException(); }
}

Esta solução tem um problema apontado por @supercat ilustrado aqui:

var dict = new Dictionary<int, string>
{
    { 1, "one" },
    { 2, "two" },
    { 3, "three" },
};

var rodict = new ReadOnlyDictionary<int, string>(dict);
var rwdict = rodict as Dictionary<int, string>;
rwdict.Add(4, "four");

foreach (var item in rodict)
{
    Console.WriteLine("{0}, {1}", item.Key, item.Value);
}

Em vez de fornecer um erro em tempo de compilação, como eu esperava, ou uma exceção de tempo de execução, como eu esperava, esse código é executado sem erros. Imprime quatro números. Isso faz do meu ReadOnlyDictionary um ReadWriteDictionary.

user2023861
fonte
O problema com essa abordagem é que esse objeto pode ser passado para um método que espera Dictionary<TKey,TValue>sem queixas do compilador, e lançar ou coagir uma referência ao tipo de dicionário simples removerá as proteções.
Supercat
@ Supercat, porcaria, você está certo. Eu pensei que tinha uma boa solução também.
User2023861
Lembro-me de fazer uma derivada de Dictionarycom um Clonemétodo acorrentado MemberwiseClone. Infelizmente, embora seja possível clonar um dicionário de forma eficiente, clonando os repositórios, o fato de que os repositórios são privatemais do que protectedsignifica que não há como uma classe derivada cloná-los; usar MemberwiseClonesem também clonar as lojas de backup significa que as modificações subseqüentes feitas no dicionário original quebrarão o clone e as modificações feitas no clone quebrarão o original.
Supercat