لماذا لا يستخدم C#الخصائص المفهرسة؟




language-features indexed-properties (6)

أعلم ، أعرف ... إجابة إريك ليبرت لهذا النوع من الأسئلة هي عادة مثل " لأنه لم يكن يستحق كلفة تصميم وتنفيذ واختبار وتوثيقه ".

ولكن لا يزال ، أود توضيح أفضل ... كنت أقرأ هذا بلوق عن الميزات الجديدة C # 4 ، وفي المقطع حول COM Interop ، لفت انتباهي الجزء التالي:

بالمناسبة ، يستخدم هذا الرمز ميزة جديدة أكثر: خصائص مفهرسة (إلقاء نظرة فاحصة على تلك الأقواس المربعة بعد المدى.) ولكن هذه الميزة متوفرة فقط لـ COM interop؛ لا يمكنك إنشاء الخصائص المفهرسة الخاصة بك في C # 4.0 .

حسنا لكن لماذا ؟ لقد عرفت بالفعل وندمت على أنه لم يكن من الممكن إنشاء خصائص مفهرسة في C # ، ولكن هذه الجملة جعلتني أفكر مرة أخرى في ذلك. أستطيع أن أرى عدة أسباب وجيهة لتنفيذها:

  • يدعم CLR (على سبيل المثال ، PropertyInfo.GetValue يحتوي على معلمة index ) ، لذلك من المؤسف أننا لا نستطيع الاستفادة منه في C #
  • يتم دعمه للتشغيل المتداخل COM ، كما هو موضح في المقالة (باستخدام التوزيع الديناميكي)
  • يتم تنفيذه في VB.NET
  • من الممكن بالفعل إنشاء مفهرسين ، أي تطبيق فهرس على الكائن نفسه ، لذا من غير المحتمل أن يكون هناك صفقة كبيرة لتوسيع الفكرة إلى خصائص ، مع الحفاظ على نفس البنية واستبدالها فقط باسم الخاصية

سيسمح لك بكتابة هذا النوع من الأشياء:

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 #؟ إذا لم يكن كذلك ، لماذا؟


AC # indexer هو خاصية مفهرسة. يدعى Item بشكل افتراضي (ويمكنك الرجوع إليه على سبيل المثال من VB) ، ويمكنك تغييره مع IndexerNameAttribute إذا أردت.

لست متأكدا لماذا ، على وجه التحديد ، تم تصميمه بهذه الطريقة ، ولكن يبدو أنها حدود مقصودة. ومع ذلك ، فهي تتفق مع إرشادات تصميم الإطار ، التي توصي بنهج الخاصية غير المفهرسة التي تعرض كائن قابل للفهرسة لمجموعات الأعضاء. أي "يجري فهرسة" هي سمة من نوع. إذا كان قابلاً للفهرسة بأكثر من طريقة ، فيجب تقسيمها إلى عدة أنواع.


أجد عدم وجود خصائص مفهرسة محبط للغاية عند محاولة كتابة رمز نظيفة ومختصرة. تمتلك الخاصية المفهرسة دلالة مختلفة تمامًا عن توفير مرجع للفئة يتم فهرسته أو تقديم طرق فردية. أجد أنه من المزعج بعض الشيء أن توفير الوصول إلى كائن داخلي يقوم بتنفيذ خاصية مفهرسة يعتبر مقبولاً لأن ذلك غالباً ما يكسر أحد المكونات الرئيسية في اتجاه الكائن: التغليف.

أواجه هذه المشكلة في كثير من الأحيان ، ولكنني واجهتها مرة أخرى اليوم ، لذا سأقدم مثالًا حقيقيًا على رمز عالمي. واجهة وفئة يتم تكوين تكوين مخازن مكتوبة وهي عبارة عن مجموعة من المعلومات ذات الصلة فضفاضة. كنت بحاجة إلى إضافة أجزاء من البرامج النصية المسماة واستخدام مفهرس فئة غير مسمى يعني سياقًا غير صحيح لأن أجزاء البرامج النصية ليست سوى جزء من التكوين.

إذا كانت الخصائص المفهرسة متاحة في C # ، فقد كان من الممكن تنفيذ الشفرة التالية (بناء الجملة هو هذا [المفتاح] الذي تم تغييره إلى PropertyName [مفتاح]).

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

اعتدت على تفضيل فكرة الخصائص المفهرسة ولكن أدركت بعد ذلك أنها ستضيف غموضًا فظيعًا وتثبط فعليًا الوظائف. تعني الخصائص المفهرسة أنه ليس لديك مثيل مجموعة تابعة. هذا على حد سواء جيدة وسيئة. إنها مشكلة أقل للتنفيذ ولا تحتاج إلى مرجع إلى فئة المالك المرفق. لكنه يعني أيضًا أنه لا يمكنك تمرير مجموعة الأطفال تلك إلى أي شيء ؛ من المرجح أن تعداد كل مرة. ولا يمكنك القيام به foreach على ذلك. الأسوأ من ذلك كله ، لا يمكنك معرفة ما إذا كنت تبحث عن خاصية مفهرسة ، سواء كانت تلك الخاصية أو تلك الخاصية.

الفكرة عقلانية ولكنها تؤدي فقط إلى عدم المرونة والحرج المفاجئ.


حسنًا ، أود أن أقول إنهم لم يضيفوها لأنها لا تستحق تكلفة تصميمها وتنفيذها واختبارها وتوثيقها.

وإذا نظرنا إلى المزاح جانباً ، فمن المحتمل أن يكون ذلك بسبب أن الحلول البسيطة بسيطة ، وأن الميزة لا تقلل من الوقت مقارنة بالفوائد. لن أكون مندهشًا لرؤية هذا يبدو كتغيير أسفل الخط رغم ذلك.

نسيت أيضًا أن تذكر أن الحل الأسهل هو مجرد إجراء طريقة عادية:

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

نظرًا لأنك تستطيع بالفعل القيام بهذا النوع من العمل ، وقد أجبرتك على التفكير في جوانب OO ، فإن إضافة الخصائص المفهرسة سيضيف المزيد من الضوضاء إلى اللغة. وفقط طريقة أخرى لفعل شيء آخر.

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

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

foo.Values[0]

أنا شخصياً أفضل أن أرى طريقة واحدة فقط للقيام بشيء ما ، بدلاً من 10 طرق. لكن بالطبع هذا رأي شخصي.


هناك حل عام بسيط باستخدام lambdas لتفعيل وظيفة الفهرسة

للقراءة فقط الفهرسة

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




indexed-properties