更新此方案的通用类型属性的最佳方法

时间:2017-05-12 06:50:50

标签: c# generics

我正在添加一些我希望在应用程序范围内使用的本地化逻辑,而不是多次创建它我决定为这个场景创建一个Generic方法而它可以工作,但我的Generics知识是有限的,它感觉不到最佳...

方法:

public List<T> Localize<T>(List<T> inputList, string propertyName)
{
    var filteredList = new List<T>();

    foreach (var item in inputList)
    {
        var clone = Mapper.Map<T, T>(item);
        // "TranslationKey" is the fixed/same column name for every entity/table.
        var translationKey = item.GetType().GetProperty("TranslationKey").GetValue(clone).ToString();

        if (!string.IsNullOrEmpty(translationKey))
        {
            var prop = clone.GetType().GetProperty(propertyName);
            prop.SetValue(clone, TranslateAsync(translationKey).Result);
        }
        filteredList.Add(clone);
    }
    return filteredList;
}

然后我会这样调用这个方法:

var items = Localize(itemsList, "Description"); // To localize "Description" prop
var people = Localize(peopleList, "Name"); // To localize "Name" prop of People list...

所以我的两个主要问题是:

  1. 我可以优化我的方法实现吗?
  2. 有没有办法在调用方法时不能对属性名称进行硬编码,或者是否有更好/更快的LINQ方式或者我缺少的东西?

5 个答案:

答案 0 :(得分:1)

  

我可以优化我的方法实现吗?

那么,这取决于你所说的“优化”。我认为您可以将foreach循环转换为LINQ Select调用,然后调用Where来过滤掉空值。

  

有没有办法在调用方法时无法对属性名称进行硬编码,或者是否有更好/更快的LINQ方式或者我缺少的东西?

我建议您在传递属性名称时使用nameof运算符,而不是:

var items = Localize(itemsList, "Description");

你可以这样做:

var items = Localize(itemsList, nameof(Description));

这样,您可以避免对字符串进行硬编码!

答案 1 :(得分:1)

从您的设计看来,AES-GCM可以用于为每个实体准确地本地化一个属性,因此以下内容没有意义:

TranslationKey

如果是这种情况 - 调用方法时不需要指定属性进行本地化。相反,使用这样的界面:

Localize(itemsList, "Description");
Localize(itemsList, "Title"); // cannot do that, because only description is localizable

您的public interface ITranslatable { string TranslationKey { get; } string TranslatedProperty { get; set; } } class MyEntity : ITranslatable { public string TranslationKey { get; set; } public string Description { get; set; } // implicit implementation to avoid clutter string ITranslatable.TranslatedProperty { get { return Description; } set { Description = value; } } } 方法不再需要任何反映:

Localize

答案 2 :(得分:1)

您可以解决以下几点:

1。 TranslationKey属性

在泛型方法中使用类似item.GetType().GetProperty("TranslationKey").GetValue(clone)的内容时,这意味着类型始终具有TranslationKey属性。我建议使用一些接口和泛型约束来提高速度并添加编译时检查。例如:

public interface ITranslatable
{
     string TranslationKey { get; }
}

用法:

public void Translate<T>(T value) where T : ITranslatable
{
    var translationKey = value.TranslationKey;
    ...
}

但这可能无法实现,尤其是在使用第三方类型时。

2。在每次迭代中检索PropertyInfo

您的代码似乎始终为传入的集合的每个元素访问相同的属性:

foreach (var item in items)
{
    var clone = Mapper.Map<T, T>();
    ...
        var prop = clone.GetType().GetProperty(propertyName);
    ...
}

您只需为foreach之外的每个方法调用加载一次属性,这样可以节省一些时间(特别是在大集合上)。如果你想进一步优化速度,你也可以缓存给定类型的属性,但我不建议它作为启动器。

如果Mapper.Map<T, T>可以实际获取并返回比T更多派生的类型并隐藏propertyName属性(例如,通过new关键字),则可能无效

3。清单大小

其他小问题是您分配filteredList没有任何大小提示,但大小已知,大小为inputList。这可以节省大量收藏的时间。此外,inputList参数的类型可以是IReadOnlyCollection,从而改进了签名以反映其所做的工作。

LINQ

由于prop.SetValue(clone, TranslateAsync(translationKey).Result);循环中存在foreach副作用,我反对此处使用LINQ。您仍然可以从方法返回IReadOnlyCollectionIReadOnlyList,从而在使用位置上获得更好的流量。

最终代码

public IReadOnlyCollection<T> Localize<T>(IReadOnlyCollection<T> inputList, string propertyName) where T : ITranslatable
{
    var prop = typeof(T).GetProperty(propertyName);

    var filteredList = new List<T>(inputList.Count);

    foreach (var item in inputList)
    {
        var clone = Mapper.Map<T, T>(item);
        // "TranslationKey" is the fixed/same column name for every entity/table.
        var translationKey = clone.TranslationKey;
        if (!string.IsNullOrEmpty(translationKey))
        {
            prop.SetValue(clone, TranslateAsync(translationKey).Result);
        }

        filteredList.Add(clone);
    }
    return filteredList;
}

答案 3 :(得分:1)

由于反射用于getter / setter,这可能是可以避免的,因此确实存在优化空间,更重要的是在将来更改对象的情况下使代码更安全。

第一个反射 item.GetType().GetProperty("TranslationKey").GetValue(clone)表示类型T始终具有属性TranslationKey。只要所有对象都实现了正确的接口(或共享相同的基类),通用约束就可以强制执行 例如,Item类和People类都实现类似interface ITranslationEntity{string TranslationKey{get;}}的内容,如果方法签名包含where T:ITranslationEntity,则可以直接访问该属性

setter 可以使用相同的接口实现来完成,例如在接口上有一个SetTranslatedValue(string),或者setter可以实现为lambda public List<T> Localize<T>(List<T> inputList, Action<T,string> setter)在这里使用后者作为一个例子,但在接口中使用它本身似乎是更好的选择。

一个小的额外更改可以使调用更容易,但取决于您是否可以将此方法放在静态类中,是使方法成为扩展方法。

也许最重要的部分是 async 调用在循环中同步执行(通过访问Result)。也许实现不是真正的异步,但如果是,主要的优化是组合异步调用并等待任务的集合(并将结果设置在ContinueWith

最终结果:

static class SomeClass
{

    public static List<T> Localize<T>(this List<T> inputList, Action<T,string> setter)
        where T:ITranslationEntity
    {
        var clonedList = inputList.Select(Mapper.Map<T,T>).ToList();
        var tasks = (from clone in clonedList
            let key = clone.TranslationKey
            where key != null
            select TranslateAsync(key).ContinueWith(t=> setter(clone,t.Result))).ToArray();
        Task.WaitAll(tasks);
        return clonedList;
    }
}

示例电话:

var items = itemsList.Localize((i,val) =>i.Description = val);

同样,setter只是一种替代方案,并且在其他方​​面实现可能更好,但主要的一点是尽可能避免反射,如果为多个项调用异步代码,则等待组合任务的结果。

答案 4 :(得分:0)

添加这个答案(来自其他答案)作为未来的另一种选择,这样我可以将Localize暴露给Singular和Multiple对象:

public List<T> Localize<T>(List<T> inputList, Action<T, string> setter) where T : ITranslatable
{
    return inputList.Select(a => Localize(a, setter)).ToList();
}

public T Localize<T>(T input, Action<T, string> setter) where T : ITranslatable
{
    var clone = Mapper.Map<T, T>(input);
    var translationKey = clone.TranslationKey;
    if (!string.IsNullOrEmpty(translationKey))
    {
        setter(clone, TranslateAsync(translationKey).Result);
    }
    return clone;
}   

界面:

public interface ITranslatable
{
    string TranslationKey{ get; }
}

这样打电话:

Localize(items, (i, val) => i.Description= val); // etc.