Задача: можете ли вы сделать эту простую функцию более элегантной, используя С# 4.0
Как я взломал нашу базу кода, я просто заметил эту функцию. Он преобразует IDictionary<string, object>
(Parameters - переменную экземпляра) в строку XML.
Это не что иное, как любопытство с моей стороны:-).
Так может ли он быть написан с гораздо меньшим количеством кода с использованием С# 4.0? Правило: нет внешних библиотек, кроме .Net Framework BCL.
Чтобы сделать это более сложной задачей, я не помещаю здесь спецификацию входного словаря, так как вы должны иметь возможность ее обработать из кода.
public string ConvertToXml() {
XmlDocument doc = new XmlDocument();
doc.LoadXml("<?xml version='1.0' encoding='utf-8'?><sc/>");
foreach (KeyValuePair<string, object> param in Parameters) {
XmlElement elm = doc.CreateElement("pr");
if (param.Value is int || param.Value is Int32 || param.Value is Int16 || param.Value is Int64) {
elm.SetAttribute("tp", "int");
} else if (param.Value is DateTime?){
elm.SetAttribute("tp", "datetime");
} else {
elm.SetAttribute("tp", "string");
}
elm.SetAttribute("nm", param.Key);
if (param.Value is DateTime?) {
DateTime? dateTime = param.Value as DateTime?;
elm.SetAttribute("vl", dateTime.Value.ToString("o"));
} else{
elm.SetAttribute("vl", param.Value.ToString());
}
doc.FirstChild.NextSibling.AppendChild(elm);
}
return doc.OuterXml;
}
Позвольте мне добавить еще несколько мыслей.
Мне:
- меньше, но кратковременно плохо.
- больше типов прекрасны, но тривиальные типы кажутся вонючими.
- возможность повторного использования
Ответы
Ответ 1
Использование динамических и LINQ для XML:
ConvertToXml можно свести к одному из операторов (при условии, что исключение объявления XML приемлемо).
public string ConvertToXml()
{
return new XElement("sc",
Parameters.Select(param => CreateElement(param.Key, (dynamic)param.Value))
).ToString(SaveOptions.DisableFormatting);
}
Обратите внимание, что CreateElement передает параметр param.Value в динамическое, чтобы во время выполнения была выбрана правильная перегрузка из следующих.
XElement CreateElement(string key, object value)
{
return CreateElement("string", key, value.ToString());
}
XElement CreateElement(string key, long value)
{
return CreateElement("int", key, value.ToString());
}
XElement CreateElement(string key, DateTime value)
{
return CreateElement("datetime", key, value.ToString("o"));
}
Перегруженные выше вызовы в конечном счете вызывают:
XElement CreateElement(string typename, string key, string value)
{
return new XElement("pr",
new XAttribute("tp", typename),
new XAttribute("nm", key),
new XAttribute("vl", value)
);
}
Этот код уменьшает количество операторов (хотя и не строк), найденных в вопросе. Этот подход основан на svick, но уменьшает количество требуемых методов и динамических вызовов.
Ответ 2
Использование LINQ to XML может сделать это очень простым для записи. Предпочитайте это по сравнению со стандартными библиотеками XML, если у вас есть выбор.
Я считаю, что это должно быть эквивалентно:
public static string ToXmlString(this IDictionary<string, object> dict)
{
var doc = new XDocument(new XElement("sc", dict.Select(ToXElement)));
using (var writer = new Utf8StringWriter())
{
doc.Save(writer); // "hack" to force include the declaration
return writer.ToString();
}
}
class Utf8StringWriter : StringWriter
{
public override Encoding Encoding { get { return Encoding.UTF8; } }
}
static XElement ToXElement(KeyValuePair<string, object> kvp)
{
var value = kvp.Value ?? String.Empty;
string typeString;
string valueString;
switch (Type.GetTypeCode(value.GetType()))
{
case TypeCode.Int16:
case TypeCode.Int32:
case TypeCode.Int64:
typeString = "int";
valueString = value.ToString();
break;
case TypeCode.DateTime:
typeString = "datetime";
valueString = ((DateTime)value).ToString("o");
break;
default:
typeString = "string";
valueString = value.ToString();
break;
}
return new XElement("pr",
new XAttribute("tp", typeString),
new XAttribute("nm", kvp.Key),
new XAttribute("vl", valueString));
}
Обратите внимание, что проверка того, что значение имеет тип DateTime?
, бессмысленно. Я не уверен, какая ценность в хранении значений null
в словаре, но если это было возможно, вы потеряли бы эту информацию о типе в силу создания значений типа object
.
Кроме того, если существовало значение DateTime?
, которое не было null
, тогда само значение было бы помещено в бокс, а не сама структура Nullable<DateTime>
. Таким образом, фактический тип будет DateTime
, поэтому этот код работает.
Ответ 3
Использование таких функций .net 4.0, как Tuple
и dynamic
. Мои тестовые примеры дают точный результат исходного вопроса.
//using System;
//using System.Collections.Generic;
//using System.Linq;
//using System.Xml.Linq;
public string ConvertToXml()
{
//Create XDocument to specification with linq-to-xml
var doc = new XDocument(
new XElement("sc",
from param in Parameters
//Uses dynamic invocation to use overload resolution at runtime
let attributeVals = AttributeValues((dynamic)param.Value)
select new XElement("pr",
new XAttribute("tp", attributeVals.Item1),
new XAttribute("nm", param.Key),
new XAttribute("vl", attributeVals.Item2)
)
)
);
//Write to string
using (var writer = new Utf8StringWriter())
{
doc.Save(writer, SaveOptions.DisableFormatting);//Don't add whitespace
return writer.ToString();
}
}
//C# overloading will choose `long` as the best pick for `short` and `int` types too
static Tuple<string, string> AttributeValues(long value)
{
return Tuple.Create("int", value.ToString());
}
//Overload for DateTime
static Tuple<string, string> AttributeValues(DateTime value)
{
return Tuple.Create("datetime", value.ToString("o"));
}
//Overload catch all
static Tuple<string, string> AttributeValues(object value)
{
return Tuple.Create("string", value.ToString());
}
// Using John Skeet Utf8StringWriter trick
// http://stackoverflow.com/questions/3871738/force-xdocument-to-write-to-string-with-utf-8-encoding/3871822#3871822
class Utf8StringWriter : System.IO.StringWriter
{
public override System.Text.Encoding Encoding { get { return System.Text.Encoding.UTF8; } }
}
Необязательно:
Изменить оператор let:
let attributeVals = (Tuple<string,string>)AttributeValues((dynamic)param.Value)
Это ограничило бы динамический вызов только этой строкой. Но, поскольку не происходит многого другого, я подумал, что было бы более чистым смотреть, чтобы не добавлять дополнительный бросок.
Ответ 4
public string ConvertToXml()
{
var doc = new XDocument(
new XElement("sd",
Parameters.Select(param =>
new XElement("pr",
new XAttribute("tp", GetTypeName((dynamic)param.Value)),
new XAttribute("nm", param.Key),
new XAttribute("vl", GetValue((dynamic)param.Value))
)
)
)
);
return doc.ToString();
}
Этот код предполагает, что вы перегружали методы GetTypeName()
и GetValue()
, реализованные как:
static string GetTypeName(long value)
{
return "int";
}
static string GetTypeName(DateTime? value)
{
return "datetime";
}
static string GetTypeName(object value)
{
return "string";
}
static string GetValue(DateTime? value)
{
return value.Value.ToString("o");
}
static string GetValue(object value)
{
return value.ToString();
}
Это использует тот факт, что при использовании dynamic
правильная перегрузка будет выбрана во время выполнения.
Вам не нужны перегрузки для int
и short
, потому что они могут быть преобразованы в long
(и такое преобразование считается лучше, чем преобразование в object
). Но это также означает, что такие типы, как ushort
и byte
, получат tp
of int
.
Кроме того, возвращаемая строка не содержит объявления XML, но не имеет смысла объявлять, что кодированная UTF-16 строка кодируется UTF-8. (Если вы хотите сохранить его в кодированном UTF-8 файле позже, возврат и сохранение XDocument
будет лучше и напишет правильное объявление XML.)
Я думаю, что это хорошее решение, потому что оно прекрасно разделяет проблемы на разные методы (вы даже можете перегрузить GetTypeName()
и GetValue()
в другой класс).
Ответ 5
Подходим к рассмотрению новых требований.
- Детали разделенных преобразований каждого конкретного типа и самой логики генерации XML
- Легко можно было бы ввести новую поддержку типа данных, добавив к поставщику новый factory. В настоящее время поддерживаемые типы типов ограничены членами TypeCode, но, очевидно, это можно легко переключить на другой селектор/идентификатор типа.
- Я должен согласиться с jbtule, что Tuple.Create() действительно выглядит намного лучше, чем построение KeyValuePair <, > , никогда не использовал его раньше, приятный материал, спасибо!
Сам метод:
public string ConvertToXml(
IDictionary<string, object> rawData,
Dictionary<TypeCode, Func<object, Tuple<string, string>>> transformationFactoryProvider)
{
XmlDocument doc = new XmlDocument();
doc.LoadXml("<?xml version='1.0' encoding='utf-8'?><sc/>");
if (rawData != null)
{
Func<object, Tuple<string, string>> defaultFactory =
(raw) => Tuple.Create("string", raw.ToString());
foreach (KeyValuePair<string, object> item in rawData)
{
TypeCode parameterTypeCode = Type.GetTypeCode(item.Value.GetType());
var transformationFactory = transformationFactoryProvider.ContainsKey(parameterTypeCode)
? transformationFactoryProvider[parameterTypeCode]
: defaultFactory;
var transformedItem = transformationFactory(item.Value);
XmlElement xmlElement = doc.CreateElement("pr");
xmlElement.SetAttribute("tp", transformedItem.Item1);
xmlElement.SetAttribute("nm", item.Key);
xmlElement.SetAttribute("vl", transformedItem.Item2);
doc.FirstChild.NextSibling.AppendChild(xmlElement);
}
}
return doc.OuterXml;
}
Практический пример:
// Transformation Factories
// Input: raw object
// Output: Item1: type name, Item2: value in the finally formatted string
Func<object, Tuple<string, string>> numericFactory = raw => Tuple.Create("int", raw.ToString());
Func<object, Tuple<string, string>> dateTimeFactory =
raw => Tuple.Create("datetime", (raw as DateTime?).GetValueOrDefault().ToString("o"));
// Transformation Factory Provider
// Input: TypeCode
// Output: transformation factory for the given type
var transformationFactoryProvider =
new Dictionary<TypeCode, Func<object, Tuple<string, string>>>
{
{TypeCode.Int16, numericFactory},
{TypeCode.Int32, numericFactory},
{TypeCode.Int64, numericFactory},
{TypeCode.DateTime, dateTimeFactory}
};
// Convert to XML given parameters
IDictionary<string, object> parameters = new Dictionary<string, object>
{
{ "SOMEDATA", 12 },
{ "INTXX", 23 },
{ "DTTM", DateTime.Now },
{ "PLAINTEXT", "Plain Text" },
{ "RAWOBJECT", new object() },
};
string xmlParameters = this.ConvertToXml(parameters, transformationFactoryProvider);