Práticas recomendadas retornando objeto somente leitura

11

Eu tenho a pergunta "melhores práticas" sobre POO em C # (mas isso se aplica a todos os idiomas).

Considere ter uma classe de biblioteca com um objeto a ser exposto ao público, por exemplo, via acessador de propriedade, mas não queremos que o público (pessoas que usam essa classe de biblioteca) o altere.

class A
{
    // Note: List is just example, I am interested in objects in general.
    private List<string> _items = new List<string>() { "hello" }

    public List<string> Items
    {
        get
        {
            // Option A (not read-only), can be modified from outside:
            return _items;

            // Option B (sort-of read-only):
            return new List<string>( _items );

            // Option C, must change return type to ReadOnlyCollection<string>
            return new ReadOnlyCollection<string>( _items );
        }
    }
}

Obviamente, a melhor abordagem é a "opção C", mas muito poucos objetos possuem a variante ReadOnly (e certamente nenhuma classe definida pelo usuário a possui).

Se você fosse usuário da classe A, esperaria mudanças no

List<string> someList = ( new A() ).Items;

propagar para o objeto original (A)? Ou está certo devolver um clone desde que tenha sido escrito em comentários / documentação? Eu acho que essa abordagem de clone pode levar a erros bastante difíceis de rastrear.

Lembro que em C ++ poderíamos retornar objetos const e você só poderia chamar métodos marcados como const nele. Eu acho que não existe esse recurso / padrão no c #? Se não, por que eles não o incluiriam? Eu acredito que é chamado const correção.

Mas, novamente, minha pergunta principal é sobre "o que você esperaria" ou como lidar com a Opção A vs B.

Nunca pare de aprender
fonte
2
Dê uma olhada neste artigo sobre esta visualização de novas coleções imutáveis ​​para o .NET 4.5: blogs.msdn.com/b/bclteam/archive/2012/12/18/… Você já pode obtê-las pelo NuGet.
30513 Kristof Claes

Respostas:

10

Além da resposta de @ Jesse, que é a melhor solução para coleções: a idéia geral é projetar sua interface pública de A de uma maneira que retorne apenas

  • tipos de valor (como int, double, struct)

  • objetos imutáveis (como "string" ou classes definidas pelo usuário que oferecem apenas métodos somente leitura, assim como sua classe A)

  • (somente se você precisar): cópias de objetos, como sua "Opção B", demonstram

IEnumerable<immutable_type_here> é imutável por si só, então esse é apenas um caso especial do que foi dito acima.

Doc Brown
fonte
19

Observe também que você deve programar para interfaces e, em geral, o que você deseja retornar dessa propriedade é um IEnumerable<string>. A List<string>é um pouco de não-não, porque geralmente é mutável e vou até agora dizer que, ReadOnlyCollection<string>como um implementador de, ICollection<string>parece que viola o Princípio de Substituição de Liskov.

Jesse C. Slicer
fonte
6
+1, usar um IEnumerable<string>é exatamente o que se deseja aqui.
Doc Brown
2
No .Net 4.5, você também pode usar IReadOnlyList<T>.
svick
3
Nota para outras partes interessadas. Ao considerar o acessador de propriedade: se você retornar IEnumerable <string> em vez de List <string>, retornando _list (+ conversão implícita em IEnumerable), essa lista poderá ser convertida novamente em List <string> e alterada, e as alterações serão alteradas. propagar para o objeto principal. Você pode retornar a nova lista <string> (_list) (+ conversão implícita subsequente para IEnumerable) que protegerá a lista interna. Mais sobre isso pode ser encontrado aqui: stackoverflow.com/questions/4056013/…
NeverStopLearning
1
É melhor exigir que qualquer destinatário de uma coleção que precise acessá-lo por índice esteja preparado para copiar os dados em um tipo que facilite isso, ou é melhor exigir que o código que retorna a coleção forneça-o de uma forma que está acessível por índice? A menos que o fornecedor possa tê-lo de uma forma que não facilite o acesso por índice, consideraria que o valor de liberar os destinatários da necessidade de fazer sua própria cópia redundante excederia o valor de liberar o fornecedor de fornecer um formulário de acesso aleatório (por exemplo, somente leitura IList<T>)
supercat
2
Uma coisa que eu não gosto no IEnumerable é que você nunca sabe se é executado como uma rotina ou se é apenas um objeto de dados. Se funcionar como uma rotina, poderá resultar em erros sutis, portanto é bom saber algumas vezes. É por isso que eu prefiro o ReadOnlyCollection ou o IReadOnlyList etc.
Steve Vermeulen
3

Eu encontrei um problema semelhante há algum tempo atrás e abaixo foi a minha solução, mas realmente só funciona se você controlar a biblioteca também:


Comece com sua turma A

public class A
{
    private int _n;
    public int N
    {
        get { return _n; }
        set { _n = value; }
    }
}

Em seguida, derive uma interface disso apenas com a parte get das propriedades (e quaisquer métodos de leitura) e faça com que A implemente

public interface IReadOnlyA
{
    public int N { get; }
}
public class A : IReadOnlyA
{
    private int _n;
    public int N
    {
        get { return _n; }
        set { _n = value; }
    }
}

Então é claro que quando você deseja usar a versão somente leitura, uma conversão simples é suficiente. É exatamente isso que você solucione C, realmente, mas pensei em oferecer o método com o qual o alcancei

Andy Hunt
fonte
2
O problema é que é possível convertê-lo de volta para a versão mutável. E sua violação do LSP, estado de causa observável através dos métodos da classe base (neste caso, da interface) pode ser alterada por novos métodos da subclasse. Mas pelo menos comunica intenção, mesmo que não a imponha.
user470365
1

Para quem encontra esta pergunta e ainda está interessado na resposta - agora há outra opção que está expondo ImmutableArray<T>. Estes são alguns pontos pelos quais eu acho que é melhor IEnumerable<T>ou ReadOnlyCollection<T>:

  • afirma claramente que é imutável (API expressiva)
  • ele afirma claramente que a coleção já está formada (em outras palavras, não é inicializada preguiçosamente e não há problema em iterá-la várias vezes)
  • se a contagem de itens é conhecido, ele não esconde que knowlegde como IEnumerable<T>faz
  • como o cliente não sabe o que é a implementação IEnumerableou IReadOnlyCollectnão pode assumir que nunca muda (em outras palavras, não há garantia de que os itens de uma coleção não tenham sido alterados enquanto a referência a essa coleção permanece inalterada)

Além disso, se a APi precisar notificar o cliente sobre alterações na coleção, eu exporia um evento como um membro separado.

Note que não estou dizendo que isso IEnumerable<T>seja ruim em geral. Eu ainda o usaria ao passar a coleção como argumento ou ao retornar uma coleção inicializada preguiçosamente (nesse caso em particular, faz sentido ocultar a contagem de itens porque ainda não é conhecido).

Pavels Ahmadulins
fonte