Por que C # não implementa propriedades indexadas?

83

Eu sei, eu sei ... A resposta de Eric Lippert para esse tipo de pergunta é geralmente algo como " porque não valeu o custo de projetar, implementar, testar e documentar ".

Mesmo assim, gostaria de uma explicação melhor ... Eu estava lendo esta postagem do blog sobre os novos recursos do C # 4 e, na seção sobre interoperabilidade COM, a seguinte parte me chamou a atenção:

A propósito, este código usa mais um novo recurso: propriedades indexadas (observe mais de perto os colchetes após Range). Mas esse recurso está disponível apenas para interoperabilidade COM; você não pode criar suas próprias propriedades indexadas em C # 4.0 .

OK mas porquê ? Eu já sabia e me arrependia de não ter sido possível criar propriedades indexadas em C #, mas essa frase me fez pensar novamente a respeito. Posso ver vários bons motivos para implementá-lo:

  • o CLR suporta (por exemplo, PropertyInfo.GetValuetem um indexparâmetro), então é uma pena que não possamos tirar proveito disso em C #
  • é compatível com interoperabilidade COM, conforme mostrado no artigo (usando envio dinâmico)
  • é implementado em VB.NET
  • já é possível criar indexadores, ou seja, aplicar um índice ao próprio objeto, então provavelmente não seria grande coisa estender a ideia para propriedades, mantendo a mesma sintaxe e apenas substituindo thispor um nome de propriedade

Isso permitiria escrever esse tipo de coisas:

public class Foo
{
    private string[] _values = new string[3];
    public string Values[int index]
    {
        get { return _values[index]; }
        set { _values[index] = value; }
    }
}

Atualmente, a única solução alternativa que conheço é criar uma classe interna ( ValuesCollectionpor exemplo) que implemente um indexador e alterar a Valuespropriedade para que ela retorne uma instância dessa classe interna.

Isso é muito fácil de fazer, mas irritante ... Então, talvez o compilador possa fazer isso por nós! Uma opção seria gerar uma classe interna que implemente o indexador e expô-la por meio de uma interface genérica pública:

// interface defined in the namespace System
public interface IIndexer<TIndex, TValue>
{
    TValue this[TIndex index]  { get; set; }
}

public class Foo
{
    private string[] _values = new string[3];

    private class <>c__DisplayClass1 : IIndexer<int, string>
    {
        private Foo _foo;
        public <>c__DisplayClass1(Foo foo)
        {
            _foo = foo;
        }

        public string this[int index]
        {
            get { return _foo._values[index]; }
            set { _foo._values[index] = value; }
        }
    }

    private IIndexer<int, string> <>f__valuesIndexer;
    public IIndexer<int, string> Values
    {
        get
        {
            if (<>f__valuesIndexer == null)
                <>f__valuesIndexer = new <>c__DisplayClass1(this);
            return <>f__valuesIndexer;
        }
    }
}

Mas é claro que, nesse caso, a propriedade retornaria realmente um IIndexer<int, string>e não seria realmente uma propriedade indexada ... Seria melhor gerar uma propriedade indexada CLR real.

O que você acha ? Você gostaria de ver esse recurso em C #? Se não, por quê?

Thomas Levesque
fonte
1
Tenho a sensação de que este é mais um daqueles problemas do tipo "recebemos solicitações para X, mas não mais do que para Y" .
ChaosPandion de
1
@ChaosPandion, sim, você provavelmente está certo ... Mas esse recurso provavelmente seria muito fácil de implementar e, embora certamente não seja um "obrigatório", ele definitivamente se enquadra na categoria "bom ter"
Thomas Levesque
4
Os indexadores já são um pouco irritantes do ponto de vista do CLR. Eles adicionam um novo caso limite ao código que deseja trabalhar com propriedades, pois agora qualquer propriedade pode ter parâmetros de indexador. Acho que a implementação do C # faz sentido, já que o conceito que um indexador geralmente representa não é uma propriedade de um objeto, mas sim seu 'conteúdo'. Se você fornecer propriedades de indexador arbitrárias, estará sugerindo que a classe pode ter diferentes grupos de conteúdo, o que naturalmente leva ao encapsulamento do subconteúdo complexo como uma nova classe. Minha pergunta é: por que o CLR fornece propriedades indexadas?
Dan Bryant
1
@tk_ obrigado pelo seu comentário construtivo. Você está postando comentários semelhantes em todas as postagens sobre idiomas que não são Pascal Livre? Bem, espero que isso faça você se sentir bem consigo mesmo ...
Thomas Levesque
3
Esta é uma das poucas situações em que C ++ / CLI e VB.net são melhores que C #. Implementei muitas propriedades indexadas em meu código C ++ / CLI e agora, ao convertê-lo para C #, tenho que encontrar soluções alternativas para todos eles. :-( SUCKS !!! // Seu permitiria escrever esse tipo de coisas é o que tenho feito ao longo dos anos.
Tobias Knauss

Respostas:

122

Veja como projetamos o C # 4.

Primeiro, fizemos uma lista de todos os recursos possíveis que poderíamos pensar em adicionar à linguagem.

Em seguida, agrupamos os recursos em "isso é ruim, nunca devemos fazer isso", "isso é incrível, temos que fazer" e "isso é bom, mas não vamos fazer desta vez".

Em seguida, examinamos quanto orçamento tínhamos para projetar, implementar, testar, documentar, enviar e manter os recursos "obrigatórios" e descobrimos que estávamos 100% acima do orçamento.

Então, movemos um monte de coisas do balde "tenho que ter" para o balde "bom ter".

As propriedades indexadas nunca estiveram nem perto do topo da lista "preciso ter". Eles estão muito abaixo na lista de "boas" e flertando com a lista de "más idéias".

Cada minuto que gastamos projetando, implementando, testando, documentando ou mantendo o bom recurso X é um minuto que não podemos gastar com os recursos incríveis A, B, C, D, E, F e G. Temos que priorizar implacavelmente para que apenas faça os melhores recursos possíveis. Propriedades indexadas seriam boas, mas bom não está nem perto de bom o suficiente para realmente ser implementado.

Eric Lippert
fonte
20
Posso adicionar um voto por colocá-lo na lista ruim? Eu realmente não vejo como a implementação atual é uma limitação quando você poderia apenas expor um tipo aninhado que implementa um indexador. Eu imagino que você começará a ver muitos hackeamentos para tentar encaixar algo em ligações de dados e propriedades que deveriam ser métodos.
Josh
11
E esperançosamente o INotifyPropertyChanged autoimplementado está bem mais alto na lista do que as propriedades indexadas. :)
Josh
3
@Eric, OK, era disso que eu suspeitava ... obrigado por responder de qualquer maneira! Acho que posso viver sem propriedades indexadas, como tenho feito há anos;)
Thomas Levesque
5
@Martin: Não sou especialista em como as grandes equipes de software têm orçamentos determinados. Sua pergunta deve ser dirigida a Soma, Jason Zander ou Scott Wiltamuth, acredito que todos escrevem blogs ocasionalmente. Sua comparação com Scala é uma comparação de maçãs com laranjas; Scala não tem a maioria dos custos que o C # tem; só para citar um, ele não tem milhões de usuários com requisitos de compatibilidade com versões anteriores extremamente importantes. Eu poderia citar muitos outros fatores que podem causar grandes diferenças de custo entre C # e Scala.
Eric Lippert
12
+1: As pessoas podem aprender muito sobre como gerenciar projetos de software lendo isto. E tem apenas algumas linhas.
Brian MacKay de
22

O indexador AC # é uma propriedade indexada. É nomeado Itempor padrão (e você pode se referir a ele como, por exemplo, VB), e você pode alterá-lo com IndexerNameAttribute se quiser.

Não sei por que, especificamente, ele foi projetado dessa forma, mas parece ser uma limitação intencional. No entanto, é consistente com as diretrizes de design da estrutura, que recomendam a abordagem de uma propriedade não indexada que retorna um objeto indexável para coleções de membros. Ou seja, "ser indexável" é uma característica de um tipo; se for indexável de mais de uma maneira, ele realmente deve ser dividido em vários tipos.

Pavel Minaev
fonte
1
Obrigado! Tenho lutado repetidamente contra erros ao implementar interfaces de interoperabilidade COM que têm um indxer padrão (DISPID 0) que importa como este [int], mas cujo nome não era originalmente "Item" (às vezes é "item" ou "valor" ou semelhante ) Isso compila e executa de qualquer maneira, mas leva a FxCop CA1033 InterfaceMethodsShouldBeCallableByChildTypes avisos, problemas de conformidade CLS (identificadores diferindo apenas no caso), etc, uma vez que o nome não se encaixa. [IndexerName] é tudo o que era necessário, mas nunca consegui encontrá-lo.
puetzk
Obrigado!!! O atributo IndexerName me permitiu terminar de converter um assembly VB para C # sem quebrar as assinaturas MSIL.
Jon Tirjan
15

Como você já pode fazer isso, e é forçado a pensar em aspectos OO, adicionar propriedades indexadas apenas adicionaria mais ruído à linguagem. E apenas outra maneira de fazer outra coisa.

class Foo
{
    public Values Values { ... }
}

class Values
{
    public string this[int index] { ... }    
}

foo.Values[0]

Eu pessoalmente prefiro ver apenas uma única maneira de fazer algo, em vez de dez maneiras. Mas é claro que esta é uma opinião subjetiva.


fonte
2
+1, esta é uma maneira muito melhor de implementá-lo do que complicar a linguagem com construções VB5.
Josh
1
+1 porque é assim que eu faria. E se você fosse bom, provavelmente poderia fazer isso genérico.
Tony
10
Um problema com essa abordagem é que é possível que outro código faça uma cópia do indexador, e não está claro qual deve ser a semântica se isso acontecer. Se o código disser "var userList = Foo.Users; Foo.RemoveSomeUsers (); someUser = userList [5];" deve ser o que costumava ser o elemento [5] de Foo (antes de RemoveSomeUsers) ou depois? Se uma userList [] fosse uma propriedade indexada, ela não teria que ser exposta diretamente.
supercat
Você gosta de alocar transientes?
Joshua
9

Eu costumava favorecer a ideia de propriedades indexadas, mas depois percebi que isso acrescentaria uma ambigüidade terrível e, na verdade, desincentivaria a funcionalidade. Propriedades indexadas significam que você não tem uma instância de coleção filho. Isso é bom e ruim. É menos problemático para implementar e você não precisa de uma referência para a classe proprietária envolvente. Mas também significa que você não pode passar essa coleção filho para nada; você provavelmente terá que enumerar todas as vezes. Nem você pode fazer um foreach sobre ele. Pior de tudo, você não pode dizer olhando para uma propriedade indexada se é isso ou uma propriedade de coleção.

A ideia é racional, mas só leva à inflexibilidade e constrangimento abrupto.

Joshua A. Schaeffer
fonte
Pensamentos interessantes, mesmo que a resposta seja um pouco tardia;). Você está fazendo pontos muito bons, +1.
Thomas Levesque de
No vb.net, uma classe pode ter uma propriedade indexada e não indexada com o mesmo nome [por exemplo Bar]. A expressão Thing.Bar(5)usaria propriedade indexada Barem Thing, enquanto (Thing.Bar)(5)usaria propriedade não indexada Bare, em seguida, usaria o indexador padrão do objeto resultante. Para minha bind, permitindo Thing.Bar[5]a ser uma propriedade Thing, em vez de uma propriedade Thing.Bar, é bom desde que, entre outras coisas, é possível que em algum momento no tempo, o significado Thing.Bar[4]pode ser clara, mas o efeito adequado de ...
supercat
... algo como var temp=Thing.Bar; do_stuff_with_thing; var q=temp[4]pode não estar claro. Considere também a noção de que Thingpode conter os dados Barem um campo que pode ser um objeto imutável compartilhado ou um objeto mutável não compartilhado; uma tentativa de gravação Barquando o campo de apoio é imutável deve fazer uma cópia mutável, mas uma tentativa de leitura não deve. Se Barfor uma propriedade indexada nomeada, o getter indexado pode deixar a coleção de apoio sozinha (seja mutável ou não) enquanto o setter pode fazer uma nova cópia mutável, se necessário.
supercat
um indexador de uma propriedade não é um enumerador - é uma chave
George Birbilis
6

Acho a falta de propriedades indexadas muito frustrante ao tentar escrever um código limpo e conciso. Uma propriedade indexada tem uma conotação muito diferente de fornecer uma referência de classe que é indexada ou fornecer métodos individuais. Acho um pouco perturbador que fornecer acesso a um objeto interno que implementa uma propriedade indexada seja considerado aceitável, pois isso geralmente quebra um dos principais componentes da orientação do objeto: o encapsulamento.

Eu me deparo com esse problema com frequência, mas acabei de encontrá-lo novamente hoje, então vou fornecer um exemplo de código do mundo real. A interface e a classe que estão sendo gravadas armazenam a configuração do aplicativo, que é uma coleção de informações vagamente relacionadas. Eu precisava adicionar fragmentos de script nomeados e usar o indexador de classe sem nome implicaria em um contexto muito errado, pois os fragmentos de script são apenas parte da configuração.

Se as propriedades indexadas estivessem disponíveis em C #, eu poderia ter implementado o código abaixo (a sintaxe é esta [chave] alterada para PropertyName [chave]).

public interface IConfig
{
    // Other configuration properties removed for examp[le

    /// <summary>
    /// Script fragments
    /// </summary>
    string Scripts[string name] { get; set; }
}

/// <summary>
/// Class to handle loading and saving the application's configuration.
/// </summary>
internal class Config : IConfig, IXmlConfig
{
  #region Application Configuraiton Settings

    // Other configuration properties removed for examp[le

    /// <summary>
    /// Script fragments
    /// </summary>
    public string Scripts[string name]
    {
        get
        {
            if (!string.IsNullOrWhiteSpace(name))
            {
                string script;
                if (_scripts.TryGetValue(name.Trim().ToLower(), out script))
                    return script;
            }
            return string.Empty;
        }
        set
        {
            if (!string.IsNullOrWhiteSpace(name))
            {
                _scripts[name.Trim().ToLower()] = value;
                OnAppConfigChanged();
            }
        }
    }
    private readonly Dictionary<string, string> _scripts = new Dictionary<string, string>();

  #endregion

    /// <summary>
    /// Clears configuration settings, but does not clear internal configuration meta-data.
    /// </summary>
    private void ClearConfig()
    {
        // Other properties removed for example
        _scripts.Clear();
    }

  #region IXmlConfig

    void IXmlConfig.XmlSaveTo(int configVersion, XElement appElement)
    {
        Debug.Assert(configVersion == 2);
        Debug.Assert(appElement != null);

        // Saving of other properties removed for example

        if (_scripts.Count > 0)
        {
            var scripts = new XElement("Scripts");
            foreach (var kvp in _scripts)
            {
                var scriptElement = new XElement(kvp.Key, kvp.Value);
                scripts.Add(scriptElement);
            }
            appElement.Add(scripts);
        }
    }

    void IXmlConfig.XmlLoadFrom(int configVersion, XElement appElement)
    {
        // Implementation simplified for example

        Debug.Assert(appElement != null);
        ClearConfig();
        if (configVersion == 2)
        {
            // Loading of other configuration properites removed for example

            var scripts = appElement.Element("Scripts");
            if (scripts != null)
                foreach (var script in scripts.Elements())
                    _scripts[script.Name.ToString()] = script.Value;
        }
        else
            throw new ApplicaitonException("Unknown configuration file version " + configVersion);
    }

  #endregion
}

Infelizmente, as propriedades indexadas não são implementadas, então implementei uma classe para armazená-las e forneci acesso a ela. Esta é uma implementação indesejável porque o propósito da classe de configuração neste modelo de domínio é encapsular todos os detalhes. Os clientes desta classe acessarão fragmentos de script específicos por nome e não têm razão para contá-los ou enumerá-los.

Eu poderia ter implementado isso como:

public string ScriptGet(string name)
public void ScriptSet(string name, string value)

O que eu provavelmente deveria ter feito, mas esta é uma ilustração útil de por que usar classes indexadas como um substituto para esse recurso ausente geralmente não é um substituto razoável.

Para implementar um recurso semelhante como uma propriedade indexada, tive que escrever o código abaixo, que você notará que é consideravelmente mais longo, mais complexo e, portanto, mais difícil de ler, entender e manter.

public interface IConfig
{
    // Other configuration properties removed for examp[le

    /// <summary>
    /// Script fragments
    /// </summary>
    ScriptsCollection Scripts { get; }
}

/// <summary>
/// Class to handle loading and saving the application's configuration.
/// </summary>
internal class Config : IConfig, IXmlConfig
{
    public Config()
    {
        _scripts = new ScriptsCollection();
        _scripts.ScriptChanged += ScriptChanged;
    }

  #region Application Configuraiton Settings

    // Other configuration properties removed for examp[le

    /// <summary>
    /// Script fragments
    /// </summary>
    public ScriptsCollection Scripts
    { get { return _scripts; } }
    private readonly ScriptsCollection _scripts;

    private void ScriptChanged(object sender, ScriptChangedEventArgs e)
    {
        OnAppConfigChanged();
    }

  #endregion

    /// <summary>
    /// Clears configuration settings, but does not clear internal configuration meta-data.
    /// </summary>
    private void ClearConfig()
    {
        // Other properties removed for example
        _scripts.Clear();
    }

  #region IXmlConfig

    void IXmlConfig.XmlSaveTo(int configVersion, XElement appElement)
    {
        Debug.Assert(configVersion == 2);
        Debug.Assert(appElement != null);

        // Saving of other properties removed for example

        if (_scripts.Count > 0)
        {
            var scripts = new XElement("Scripts");
            foreach (var kvp in _scripts)
            {
                var scriptElement = new XElement(kvp.Key, kvp.Value);
                scripts.Add(scriptElement);
            }
            appElement.Add(scripts);
        }
    }

    void IXmlConfig.XmlLoadFrom(int configVersion, XElement appElement)
    {
        // Implementation simplified for example

        Debug.Assert(appElement != null);
        ClearConfig();
        if (configVersion == 2)
        {
            // Loading of other configuration properites removed for example

            var scripts = appElement.Element("Scripts");
            if (scripts != null)
                foreach (var script in scripts.Elements())
                    _scripts[script.Name.ToString()] = script.Value;
        }
        else
            throw new ApplicaitonException("Unknown configuration file version " + configVersion);
    }

  #endregion
}

public class ScriptsCollection : IEnumerable<KeyValuePair<string, string>>
{
    private readonly Dictionary<string, string> Scripts = new Dictionary<string, string>();

    public string this[string name]
    {
        get
        {
            if (!string.IsNullOrWhiteSpace(name))
            {
                string script;
                if (Scripts.TryGetValue(name.Trim().ToLower(), out script))
                    return script;
            }
            return string.Empty;
        }
        set
        {
            if (!string.IsNullOrWhiteSpace(name))
                Scripts[name.Trim().ToLower()] = value;
        }
    }

    public void Clear()
    {
        Scripts.Clear();
    }

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

    public event EventHandler<ScriptChangedEventArgs> ScriptChanged;

    protected void OnScriptChanged(string name)
    {
        if (ScriptChanged != null)
        {
            var script = this[name];
            ScriptChanged.Invoke(this, new ScriptChangedEventArgs(name, script));
        }
    }

  #region IEnumerable

    public IEnumerator<KeyValuePair<string, string>> GetEnumerator()
    {
        return Scripts.GetEnumerator();
    }

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

  #endregion
}

public class ScriptChangedEventArgs : EventArgs
{
    public string Name { get; set; }
    public string Script { get; set; }

    public ScriptChangedEventArgs(string name, string script)
    {
        Name = name;
        Script = script;
    }
}
James Higgins
fonte
2

Outra solução alternativa está listada em Criação fácil de propriedades que oferecem suporte à indexação em C # , que requer menos trabalho.

EDITAR : Devo também acrescentar que em resposta à pergunta original, que se pudermos realizar a sintaxe desejada, com suporte de biblioteca, então acho que deve haver um caso muito forte para adicioná-lo diretamente à linguagem, a fim de minimizar o inchaço da linguagem.

cdiggins
fonte
2
Em resposta à sua edição: Não acho que isso causaria inchaço da linguagem; a sintaxe já existe para indexadores de classe ( this[]), eles só precisam permitir um identificador em vez de this. Mas duvido que algum dia seja incluído na linguagem, pelos motivos explicados por Eric em sua resposta
Thomas Levesque
1

Bem, eu diria que eles não o adicionaram porque não valeu o custo de projetar, implementar, testar e documentar.

Brincadeiras à parte, provavelmente é porque as soluções alternativas são simples e o recurso nunca reduz o tempo versus benefícios. Eu não ficaria surpreso em ver isso parecer uma mudança no futuro.

Você também se esqueceu de mencionar que uma solução mais fácil é criar um método regular:

public void SetFoo(int index, Foo toSet) {...}
public Foo GetFoo(int index) {...}
Ron Warholic
fonte
Muito verdadeiro. Se a sintaxe da propriedade for absolutamente vital, você pode usar a solução alternativa do Ion (talvez com alguns genéricos para permitir uma variedade de tipos de retorno). Independentemente disso, acho que isso demonstra a relativa facilidade de realizar o mesmo trabalho sem recursos de linguagem adicionais.
Ron Warholic
1

Existe uma solução geral simples usando lambdas para representar a funcionalidade de indexação

Para indexação somente leitura

public class RoIndexer<TIndex, TValue>
{
    private readonly Func<TIndex, TValue> _Fn;

    public RoIndexer(Func<TIndex, TValue> fn)
    {
        _Fn = fn;
    }

    public TValue this[TIndex i]
    {
        get
        {
            return _Fn(i);
        }
    }
}

Para indexação mutável

public class RwIndexer<TIndex, TValue>
{
    private readonly Func<TIndex, TValue> _Getter;
    private readonly Action<TIndex, TValue> _Setter;

    public RwIndexer(Func<TIndex, TValue> getter, Action<TIndex, TValue> setter)
    {
        _Getter = getter;
        _Setter = setter;
    }

    public TValue this[TIndex i]
    {
        get
        {
            return _Getter(i);
        }
        set
        {
            _Setter(i, value);
        }
    }
}

e uma fábrica

public static class Indexer
{
    public static RwIndexer<TIndex, TValue> Create<TIndex, TValue>(Func<TIndex, TValue> getter, Action<TIndex, TValue> setter)
    {
        return new RwIndexer<TIndex, TValue>(getter, setter);
    } 
    public static RoIndexer<TIndex, TValue> Create<TIndex, TValue>(Func<TIndex, TValue> getter)
    {
        return new RoIndexer<TIndex, TValue>(getter);
    } 
}

no meu próprio código eu uso como

public class MoineauFlankContours
{

    public MoineauFlankContour Rotor { get; private set; }

    public MoineauFlankContour Stator { get; private set; }

     public MoineauFlankContours()
    {
        _RoIndexer = Indexer.Create(( MoineauPartEnum p ) => 
            p == MoineauPartEnum.Rotor ? Rotor : Stator);
    }
    private RoIndexer<MoineauPartEnum, MoineauFlankContour> _RoIndexer;

    public RoIndexer<MoineauPartEnum, MoineauFlankContour> FlankFor
    {
        get
        {
            return _RoIndexer;
        }
    }

}

e com uma instância de MoineauFlankContours posso fazer

MoineauFlankContour rotor = contours.FlankFor[MoineauPartEnum.Rotor];
MoineauFlankContour stator = contours.FlankFor[MoineauPartEnum.Stator];
bradgonesurfing
fonte
Inteligente, mas provavelmente seria melhor armazenar o indexador em cache em vez de criá-lo todas as vezes;)
Thomas Levesque
0

Acabei de descobrir também que você pode usar interfaces explicitamente implementadas para fazer isso, conforme mostrado aqui: Propriedade indexada nomeada em C #? (veja a segunda maneira mostrada nessa resposta)

George Birbilis
fonte