在尝试编写干净、简洁的代码时,我发现缺少索引属性非常令人沮丧。索引属性与提供索引的类引用或提供单独的方法具有非常不同的含义。我发现提供对实现索引属性的内部对象的访问甚至被认为是可以接受的,这有点令人不安,因为这通常会破坏面向对象的关键组件之一:封装。
我经常遇到这个问题,但我今天又遇到了,所以我将提供一个真实世界的代码示例。正在编写的接口和类存储应用程序配置,它是松散相关信息的集合。我需要添加命名的脚本片段,并且使用未命名的类索引器会暗示一个非常错误的上下文,因为脚本片段只是配置的一部分。
如果索引属性在 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;
}
}