C#がインデックス付きプロパティを実装しないのはなぜですか?


83

私は知っています、私は知っています...この種の質問に対するエリック・リペットの答えは、通常、「設計、実装、テスト、および文書化のコストに見合う価値がなかったため」のようなものです。

しかし、それでも、もっと良い説明が欲しいのですが...新しいC#4機能についてのこのブログ投稿を読んでいて、COM相互運用に関するセクションで次の部分が私の注意を引きました:

ちなみに、このコードはもう1つの新機能を使用しています。インデックス付きプロパティ(範囲の後の角括弧を詳しく見てください)。ただし、この機能はCOM相互運用でのみ使用できます。C#4.0で独自のインデックス付きプロパティを作成することはできません

いいけどなんで ?C#でインデックス付きプロパティを作成できないことはすでに知っていて後悔していましたが、この文でもう一度考えさせられました。私はそれを実装するいくつかの正当な理由を見ることができます:

  • CLRは、それが(例えば、サポートしPropertyInfo.GetValueているindexことが、我々はC#で、それを利用することはできません残念ですので、パラメータ)
  • 記事に示されているように、COM相互運用でサポートされています(動的ディスパッチを使用)
  • VB.NETに実装されています
  • インデクサーを作成すること、つまりオブジェクト自体にインデックスを適用することはすでに可能です。そのため、同じ構文を維持thisし、プロパティ名に置き換えるだけで、アイデアをプロパティに拡張することはおそらく大したことではありません。

それはそのようなことを書くことを可能にするでしょう:

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

現在私が知っている唯一の回避策はValuesCollection、インデクサーを実装する内部クラス(たとえば)を作成し、Valuesその内部クラスのインスタンスを返すようにプロパティを変更することです。

これは非常に簡単ですが、面倒です...おそらくコンパイラが私たちのためにそれを行うことができます!オプションは、インデクサーを実装する内部クラスを生成し、パブリックジェネリックインターフェイスを介して公開することです。

// 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;
        }
    }
}

しかしもちろん、その場合、プロパティは実際にはを返し、IIndexer<int, string>実際にはインデックス付きプロパティにはなりません...実際のCLRインデックス付きプロパティを生成する方がよいでしょう。

どう思いますか ?この機能をC#で表示しますか?そうでない場合、なぜですか?


1
これは、「Xのリクエストはありますが、Yのリクエストしかありません」という問題の1つだと感じています。
ChaosPandion 2010年

1
@ChaosPandion、はい、おそらくその通りです...しかし、この機能はおそらく実装が非常に簡単であり、確かに「必須」ではありませんが、間違いなく「必須」カテゴリに分類されます
Thomas Levesque

4
インデクサーは、CLRの観点からはすでに少し面倒です。プロパティを操作するコードに新しい境界ケースを追加します。これは、どのプロパティにもインデクサーパラメーターが含まれる可能性があるためです。インデクサーが通常表す概念はオブジェクトのプロパティではなく、その「コンテンツ」であるため、C#の実装は理にかなっていると思います。任意のインデクサープロパティを指定すると、クラスが異なるコンテンツグループを持つことができることを意味します。これにより、複雑なサブコンテンツが新しいクラスとしてカプセル化されます。私の質問は、CLRがインデックス付きプロパティを提供するのはなぜですか?
ダンブライアント2010年

1
@tk_建設的なコメントをありがとう。Free Pascal以外の言語に関するすべての投稿に同様のコメントを投稿していますか?まあ、それがあなた自身について気分が良くなることを願っています...
トーマスレベスク2016年

3
これは、C ++ / CLIとVB.netがC#よりも優れている数少ない状況の1つです。私はC ++ / CLIコードに多くのインデックス付きプロパティを実装しましたが、それをC#に変換する際に、それらすべての回避策を見つける必要があります。:-( SUCKS !!!あなた//これは、物事のようなものを書くことができるようになることは、私が長年にわたって行われてきたものである。
トビアスKnauss

回答:


122

C#4の設計方法は次のとおりです。

最初に、言語に追加することを考えることができるすべての可能な機能のリストを作成しました。

次に、機能を「これは悪い、絶対にやらない」、「これはすごい、やらなければならない」、「これは良いが今回はやめよう」にまとめました。

次に、「お奨め」機能の設計、実装、テスト、文書化、出荷、保守に必要な予算を調べたところ、予算を100%上回っていたことがわかりました。

そこで、たくさんのものを「お奨め」バケットから「素敵な」バケットに移動しました。

インデックス付きプロパティは決してどこでもだったの近くに「持ってお奨め」リストのトップ。彼らは「いい」リストで非常に低く、「悪い考え」リストでいちゃつく。

優れた機能Xの設計、実装、テスト、文書化、または保守に費やす1分ごとに、優れた機能A、B、C、D、E、F、Gに費やすことができない分です。可能な限り最善の機能を実行します。インデックス付きのプロパティは便利ですが、実際に実装するのに十分なほど優れているとは言えません。


20
悪いリストに載せるための投票を追加できますか?インデクサーを実装するネストされた型を公開するだけで、現在の実装がどのように制限されているのか、実際にはわかりません。メソッドであるはずのデータバインディングとプロパティに何かを押し込もうとするハッカーがたくさん見られるようになると思います。
Josh

11
そしてうまくいけば、自動実装されたINotifyPropertyChangedは、インデックス付きのプロパティよりもリストの上位にあります。:)
Josh

3
@エリック、OK、それは私が疑ったことです...とにかく答えてくれてありがとう!私は何年もやってきたように、インデックス付きのプロパティなしで生きることができると思います;)
Thomas Levesque 2010年

5
@Martin:私は大規模なソフトウェアチームの予算がどのように決定されるかについての専門家ではありません。あなたの質問は相馬、ジェイソン・ザンダーまたはスコット・ウィルタムスに向けられるべきです、私は誰もが時々ブログを書くと信じています。Scalaとの比較は、リンゴとオレンジの比較です。Scalaには、C#のようなコストのほとんどはありません。一例を挙げると、非常に重要な下位互換性要件を持つ何百万ものユーザーがいません。C#とScalaの間に大きなコスト差を引き起こす可能性のある要因をもっとたくさん挙げることができます。
Eric Lippert 2010

12
+1:これを読むことで、人々はソフトウェアプロジェクトの管理について多くを学ぶことができます。そして、それはほんの数行の長さです。
ブライアンマッケイ

22

AC#インデクサーインデックス付きのプロパティです。Itemデフォルトで名前が付けられ(VBなどからそのように参照できます)、必要に応じてIndexerNameAttributeを使用して変更できます。

具体的には、そのように設計されている理由はわかりませんが、意図的な制限のようです。ただし、これは、メンバーコレクションのインデックス可能なオブジェクトを返すインデックスのないプロパティのアプローチを推奨するフレームワーク設計ガイドラインと一致しています。つまり、「インデックス可能であること」はタイプの特性です。複数の方法でインデックスを作成できる場合は、実際にはいくつかのタイプに分割する必要があります。


1
ありがとうございました!this [int]としてインポートされるデフォルトのindxer(DISPID 0)を持ち、名前が元々「Item」ではなかった(「item」や「value」などの場合もある)COM相互運用インターフェイスを実装するときにエラーと繰り返し戦いました。 )。これはとにかくコンパイルして実行しますが、名前が完全に適合しないため、FxCop CA1033 InterfaceMethodsShouldBeCallableByChildTypes警告、CLSコンプライアンスの問題(識別子は大文字と小文字のみが異なる)などが発生します。[IndexerName]だけが必要でしたが、私はそれを見つけることができませんでした。
puetzk 2015

ありがとうございました!!!IndexerName属性を使用すると、MSIL署名を壊すことなくVBアセンブリをC#に変換し終えることができました。
Jon Tirjan 2016

15

あなたはすでにそれを行うことができ、OOの側面で考えることを余儀なくされているので、インデックス付きのプロパティを追加すると、言語にノイズが追加されるだけです。そして、別のことをするためのちょうど別の方法。

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

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

foo.Values[0]

私は個人的に、10の方法ではなく、1つの方法だけを見たいと思っています。しかしもちろん、これは主観的な意見です。


2
+1、これはVB5構造で言語を強化するよりもはるかに優れた実装方法です。
Josh

1
これが私がそれをする方法だからです。そして、あなたが良ければ、おそらくこれをジェネリックにすることができます。
トニー

10
このアプローチの問題の1つは、他のコードがインデクサーのコピーを作成する可能性があることです。また、作成する場合のセマンティクスが不明確です。コードに「varuserList = Foo.Users; Foo.RemoveSomeUsers(); someUser = userList [5];」と表示されている場合 それは、Fooの要素[5](RemoveSomeUsersの前)または後のものである必要がありますか?1つのuserList []がインデックス付きプロパティである場合、直接公開する必要はありません。
スーパーキャット2011

トランジェントを割り当てるのが好きですか?
ジョシュア

9

以前はインデックス付きのプロパティのアイデアを好んでいましたが、それが恐ろしい曖昧さを追加し、実際に機能を阻害することに気づきました。インデックス付きのプロパティは、子コレクションインスタンスがないことを意味します。それは良いことでも悪いことでもあります。実装するのはそれほど面倒ではなく、囲んでいる所有者クラスへの参照を戻す必要はありません。しかし、それはまた、その子コレクションを何にも渡すことができないことを意味します。毎回列挙する必要があるでしょう。また、foreachを実行することもできません。何よりも悪いことに、インデックス付きのプロパティを見ても、それがコレクションプロパティなのかコレクションプロパティなのかわかりません。

アイデアは合理的ですが、それは柔軟性の欠如と突然のぎこちなさをもたらすだけです。


答えが少し遅れても、興味深い考え;)。あなたは非常に良い点を作っています、+ 1。
トーマスレベスク2012

vb.netでは、クラスは同じ名前のインデックス付きプロパティとインデックスなしプロパティの両方を持つことができます[例Bar]。式Thing.Bar(5)はでインデックス付きプロパティを使用しますBarThing(Thing.Bar)(5)インデックスなしプロパティBarを使用してから、結果のオブジェクトのデフォルトのインデクサーを使用します。私のバインドではThing.Bar[5]、のプロパティでThingはなく、のプロパティになることを許可することThing.Barは良いことです。なぜなら、とりわけ、ある時点での意味Thing.Bar[4]が明確になる可能性があるからですが、...
スーパーキャット2013

...のようなものvar temp=Thing.Bar; do_stuff_with_thing; var q=temp[4]は不明確かもしれません。共有された不変オブジェクトまたは共有されていない可変オブジェクトのいずれかである可能性があるフィールドにThingデータBarを保持する可能性があるという概念も考慮してください。Barバッキングフィールドが不変であるときに書き込みを試みると、可変コピーが作成されますが、バッキングフィールドからの読み取りは行わないでください。場合Barという名前のインデックス付きプロパティで、必要に応じてセッターは新しい可変コピーを作ることができながら、インデックス付きのゲッターは、(変更可能かどうか)のみでバッキングコレクションを残すことができます。
スーパーキャット2013

プロパティのインデクサは、列挙子ではありません-それはキーである
ジョージBirbilis

6

クリーンで簡潔なコードを書き込もうとすると、インデックス付きのプロパティがないことが非常にイライラします。インデックス付きプロパティは、インデックス付きのクラス参照を提供したり、個々のメソッドを提供したりするのとは非常に異なる意味合いを持っています。インデックス付きプロパティを実装する内部オブジェクトへのアクセスを提供することは、オブジェクト指向の重要なコンポーネントの1つであるカプセル化を壊すことが多いため、許容できるとさえ見なされるのは少し気がかりです。

私はこの問題に何度も遭遇しますが、今日また問題に遭遇したので、実際のコード例を提供します。記述されているインターフェースとクラスは、ゆるやかに関連する情報のコレクションであるアプリケーション構成を格納します。名前付きスクリプトフラグメントを追加する必要がありました。スクリプトフラグメントは構成の一部にすぎないため、名前なしクラスインデクサーを使用すると、非常に間違ったコンテキストが暗示されていました。

インデックス付きのプロパティがC#で利用できる場合は、次のコードを実装できます(構文はthis [key]がPropertyName [key]に変更されています)。

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
}

残念ながら、インデックス付きのプロパティは実装されていないため、それらを格納するクラスを実装し、それにアクセスできるようにしました。このドメインモデルの構成クラスの目的はすべての詳細をカプセル化することであるため、これは望ましくない実装です。このクラスのクライアントは、名前で特定のスクリプトフラグメントにアクセスし、それらを数えたり列挙したりする理由はありません。

私はこれを次のように実装できたでしょう:

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

これはおそらく必要ですが、これは、この欠落している機能の代わりにインデックス付きクラスを使用することが合理的な代替ではないことが多い理由を示す便利な例です。

インデックス付きプロパティと同様の機能を実装するには、以下のコードを作成する必要がありました。以下のコードはかなり長く、複雑であるため、読み取り、理解、および保守が困難です。

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;
    }
}

2

別の回避策は、C#でのインデックス作成をサポートするプロパティの簡単な作成にリストされており、作業が少なくて済みます。

編集:元の質問に答えて、ライブラリのサポートを使用して目的の構文を達成できる場合は、それを言語に直接追加するための非常に強力なケースが必要だと思います。言語の肥大化を最小限に抑えます。


2
あなたの編集への回答:私はそれが言語の肥大化を引き起こすとは思わない。クラスインデクサー(this[])の構文はすでに存在します。構文は、の代わりに識別子を許可する必要がありthisます。しかし、エリックが彼の答えで説明した理由のために、それが言語に含まれることはないだろうと私は疑っています
Thomas Levesque 2010

1

設計、実装、テスト、文書化のコストに見合わなかったため、彼らはそれを追加しなかったと思います。

冗談はさておき、それはおそらく回避策が単純であり、機能が時間対利益を削減することは決してないためです。しかし、これが将来の変化として現れるのを見ても驚かないでしょう。

また、より簡単な回避策は通常の方法を作成することであることに言及するのを忘れました。

public void SetFoo(int index, Foo toSet) {...}
public Foo GetFoo(int index) {...}

非常に真実です。プロパティの構文が絶対に不可欠な場合は、Ionの回避策を使用できます(おそらく、さまざまな戻り値の型を可能にするいくつかのジェネリックを使用します)。とにかく、これは、追加の言語機能なしで同じ仕事を達成するのが比較的簡単であることを物語っていると思います。
ロンウォーホリック2010年

1

ラムダを使用してインデックス機能をプロキシする簡単な一般的なソリューションがあります

読み取り専用のインデックス作成用

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);
        }
    }
}

可変インデックス用

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);
        }
    }
}

と工場

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);
    } 
}

私自身のコードでは、次のように使用します

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;
        }
    }

}

そしてMoineauFlankContoursのインスタンスで私はできる

MoineauFlankContour rotor = contours.FlankFor[MoineauPartEnum.Rotor];
MoineauFlankContour stator = contours.FlankFor[MoineauPartEnum.Stator];

賢いですが、インデクサーを毎回作成するのではなく、キャッシュする方がおそらく良いでしょう;)
Thomas Levesque 2014年

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.