web-dev-qa-db-ja.com

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

この種の質問に対するEric Lippertの答えは、通常「設計、実装、テスト、文書化のコストに見合わなかったため」のようなものです。

それでも、もっと良い説明が欲しい...私は 新しいC#4機能に関するこのブログ投稿 を読んでいて、COM Interopに関するセクションで、次の部分が私の注目を集めました:

ちなみに、このコードではもう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#で見たいですか?そうでない場合、なぜですか?

80
Thomas Levesque

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

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

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

次に、「gotta have」機能の設計、実装、テスト、文書化、出荷、保守に必要な予算を調べ、予算を100%超えていることを発見しました。

そこで、「持っている」バケツから「持っている素敵な」バケツにたくさんのものを移動しました。

インデックス付きプロパティはどこにもありませんでしたnear "gotta have"リストの先頭。彼らは「いい」リストで非常に低く、「悪い考え」リストでいちゃつきます。

Nice機能Xの設計、実装、テスト、文書化、または保守に費やしている毎分は、素晴らしい機能A、B、C、D、E、F、Gに費やすことができない分です。可能な限り最高の機能を実行します。インデックス付きプロパティはニースですが、ニースは実際に実装するのに十分なほど良いものでもありません。

113
Eric Lippert

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

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

19
Pavel Minaev

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

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

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

foo.Values[0]

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

14
Ion Todirel

以前はインデックス付きプロパティのアイデアを好んでいましたが、それがひどいあいまいさと実際にはdisincentivize機能を追加することに気付きました。インデックス付きプロパティは、子コレクションインスタンスがないことを意味します。それは良いことでも悪いことでもあります。実装するのは面倒ではなく、親クラスへの参照は不要です。ただし、その子コレクションを何にも渡すことはできません。おそらく毎回列挙する必要があります。また、foreachを行うこともできません。最悪なのは、インデックス付きプロパティを見ても、それがコレクションプロパティであるかコレクションプロパティであるかを判断できないことです。

アイデアは合理的ですが、柔軟性に欠け、不器用になります。

8

クリーンで簡潔なコードを記述しようとすると、インデックス付きプロパティの欠如が非常にイライラします。インデックス付きプロパティには、インデックス付きのクラス参照を提供したり、個々のメソッドを提供したりする意味とは非常に異なる意味合いがあります。インデックス付きプロパティを実装する内部オブジェクトへのアクセスを提供することは、オブジェクト指向の主要なコンポーネントの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;
    }
}
5
James Higgins

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

[〜#〜] edit [〜#〜]:元の質問への応答としても追加する必要があります。シンタックス、ライブラリサポートがある場合、言語の肥大化を最小限に抑えるには、言語に直接追加する非常に強力なケースが必要だと思います。

2
cdiggins

まあ、彼らはそれを設計、実装、テスト、文書化するコストの価値がなかったので、彼らはそれを追加しなかったと言うでしょう。

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

また、簡単な回避策は、単に通常のメソッドを作成することであることを忘れていました。

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

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

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

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];
1
bradgonesurfing

ここに示すように、明示的に実装されたインターフェイスを使用してこれを達成できることもわかりました: C#? の名前付きインデックス付きプロパティその返信で)

0
George Birbilis