[c#] C #이 인덱싱 된 속성을 구현하지 않는 이유는 무엇입니까?

알아요, 알아요 … 이런 종류의 질문에 대한 Eric Lippert의 대답은 일반적으로 ” 디자인, 구현, 테스트 및 문서화 비용이 들지 않았기 때문에 “와 같습니다 .

하지만 여전히 더 나은 설명이 필요합니다 … 새로운 C # 4 기능에 대한이 블로그 게시물 을 읽고 있었으며 COM Interop에 대한 섹션에서 다음 부분이 내 관심을 끌었습니다.

그런데이 코드는 색인 된 속성 (범위 뒤의 대괄호를 자세히 살펴보십시오.)이라는 새로운 기능을 하나 더 사용합니다. 그러나이 기능은 COM interop에서만 사용할 수 있습니다. C # 4.0에서는 고유 한 인덱싱 된 속성을 만들 수 없습니다 .

좋아, 그런데 왜? C #에서 인덱싱 된 속성을 만들 수 없다는 것을 이미 알고 후회했지만이 문장은 다시 생각하게했습니다. 이를 구현해야하는 몇 가지 좋은 이유를 볼 수 있습니다.

  • CLR이이를 지원하므로 (예 PropertyInfo.GetValue: index매개 변수가 있음) C #에서이를 활용할 수없는 것은 유감입니다.
  • 문서에 표시된대로 COM interop에 대해 지원됩니다 (동적 디스패치 사용).
  • 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 #에서 보시겠습니까? 그렇지 않다면 왜?



답변

C # 4를 디자인 한 방법은 다음과 같습니다.

먼저 언어에 추가 할 수있는 모든 가능한 기능 목록을 만들었습니다.

그런 다음 기능을 “이건 나쁘다, 절대하지 말아야한다”, “이건 대단해, ​​우리가해야 해”, “이건 좋지만 이번에는하지 말자”로 분류했습니다.

그런 다음 “가져야 할”기능을 설계, 구현, 테스트, 문서화, 제공 및 유지 관리하는 데 얼마나 많은 예산이 필요한지 살펴본 결과 예산이 100 % 초과되었음을 발견했습니다.

그래서 우리는 “갖고있는”물통에서 “갖기 좋은”물통으로 많은 것들을 옮겼습니다.

인덱스 속성은 결코 어느 곳이었다 근처 은 “이 꼭”목록의 맨. 그들은 “좋은”목록에서 매우 낮고 “나쁜 생각”목록에 유혹합니다.

우리가 멋진 기능 X를 디자인, 구현, 테스트, 문서화 또는 유지하는 데 소비하는 1 분은 멋진 기능 A, B, C, D, E, F 및 G에 소비 할 수없는 시간입니다. 가능한 최고의 기능을 수행하십시오. 인덱싱 된 속성은 좋지만 실제로 구현하기에 충분할만큼 좋은 것은 아닙니다.


답변

AC # 인덱서 인덱싱 된 속성입니다. Item기본적으로 이름이 지정 되며 (예 : VB에서 참조 할 수 있음) 원하는 경우 IndexerNameAttribute로 변경할 수 있습니다 .

구체적으로 왜 그렇게 설계되었는지는 잘 모르겠지만 의도적 인 제한 인 것 같습니다. 그러나 이는 멤버 컬렉션에 대해 인덱싱 가능한 개체를 반환하는 인덱싱되지 않은 속성의 접근 방식을 권장하는 프레임 워크 디자인 지침과 일치합니다. 즉, “인덱싱 가능”은 유형의 특성입니다. 여러 가지 방법으로 인덱싱 할 수있는 경우 실제로 여러 유형으로 분할해야합니다.


답변

이미 할 수 있고 OO 측면에서 생각해야하기 때문에 색인 된 속성을 추가하면 언어에 더 많은 소음이 추가됩니다. 그리고 다른 일을하는 또 다른 방법입니다.

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

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

foo.Values[0]

저는 개인적으로 10 가지 방법보다는 한 가지 방법 만보고 싶습니다. 그러나 물론 이것은 주관적인 의견입니다.


답변

나는 색인 된 속성에 대한 아이디어를 선호했지만 그것이 끔찍한 모호성을 추가하고 실제로 기능성을 떨어 뜨릴 것이라는 것을 깨달았습니다 . 인덱싱 된 속성은 자식 컬렉션 인스턴스가 없음을 의미합니다. 그것은 좋고 나쁘다. 구현하는 데 문제가 적고 둘러싸는 소유자 클래스에 대한 참조가 필요하지 않습니다. 그러나 그것은 또한 당신이 그 자식 컬렉션을 아무것도 전달할 수 없다는 것을 의미합니다. 매번 열거해야 할 것입니다. 당신은 그것에 대해 foreach를 할 수 없습니다. 무엇보다도 인덱싱 된 속성을 살펴보면 그 속성인지 컬렉션 속성인지 알 수 없습니다.

이 아이디어는 합리적이지만 유연성이 떨어지고 갑작스러운 어색함으로 이어집니다.


답변

깔끔하고 간결한 코드를 작성하려고 할 때 색인화 된 속성이 부족하다는 사실을 알게되었습니다. 인덱싱 된 속성은 인덱싱 된 클래스 참조를 제공하거나 개별 메서드를 제공하는 것과는 매우 다른 의미를 갖습니다. 인덱싱 된 속성을 구현하는 내부 개체에 대한 액세스를 제공하는 것은 종종 개체 방향의 핵심 구성 요소 중 하나 인 캡슐화를 깨뜨리기 때문에 허용되는 것으로 간주된다는 사실이 약간 혼란 스럽습니다.

이 문제가 자주 발생하지만 오늘 다시 만났으므로 실제 코드 예제를 제공하겠습니다. 작성되는 인터페이스와 클래스는 느슨하게 관련된 정보의 모음 인 애플리케이션 구성을 저장합니다. 명명 된 스크립트 조각을 추가해야했고 명명되지 않은 클래스 인덱서를 사용하면 스크립트 조각이 구성의 일부일 뿐이므로 매우 잘못된 컨텍스트를 암시했을 것입니다.

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


답변

다른 해결 방법은 더 적은 작업이 필요한 C # 인덱싱을 지원하는 속성의 손쉬운 생성에 나열되어 있습니다.

편집 : 또한 원래 질문에 대한 응답으로 라이브러리 지원을 통해 원하는 구문을 수행 할 수 있다면 언어에 직접 추가하기 위해 매우 강력한 사례가 필요하다고 생각합니다. 언어 팽창을 최소화하십시오.


답변

디자인, 구현, 테스트 및 문서화 비용이 들지 않았기 때문에 추가하지 않았다고 말하고 싶습니다.

농담을 제쳐두고, 아마도 해결 방법이 간단하고 기능이 시간 대비 이익을 줄이지 않기 때문일 것입니다. 나는 이것이 라인 아래의 변화로 나타나는 것을보고 놀라지 않을 것입니다.

더 쉬운 해결 방법은 일반적인 방법을 만드는 것임을 언급하는 것을 잊었습니다.

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