你所见过的最好或最有趣的扩展方法使用是什么?

73

我开始真的喜欢扩展方法... 我正在想是否有人在这里发现了真正令人惊叹或聪明的例子。

我今天写的一个例子:

由于其他用户的评论而编辑:

public static IEnumerable<int> To(this int fromNumber, int toNumber) {
    while (fromNumber < toNumber) {
        yield return fromNumber;
        fromNumber++;
    }
}

这样可以将 for 循环写成 foreach 循环:

foreach (int x in 0.To(16)) {
    Console.WriteLine(Math.Pow(2, x).ToString());
}

我迫不及待地想看到其他的例子!祝你愉快!


19
你的方法大多数是Enumerable.Range(http://msdn.microsoft.com/en-us/library/system.linq.enumerable.range.aspx)的重新实现。不同之处在于Range接受起始值和计数,而你的方法接受起始值和结束值。同时,你的方法也与常规的限定惯例(<)不同,它包括了上限(<=)。最后,该方法可以反向迭代,但在实践中很少需要这样做。 - Matthew Flaschen
8
违反正常的界限惯例?胡说八道。在口语和概念中,“0到16”始终是包含在内的。在for循环中,通常使用max + 1作为条件中的数字,因为在一个5项列表中,索引从0到4,使用“< 5”比“<= 4”更有意义。 - Sander
5
阅读此处:https://dev59.com/G3VC5IYBdhLWcg3whBaj - tuinstoel
6
我认为 for(int x=0; x<=16; ++x) 对有经验的程序员来说更易读。但是,闭区间往往较少使用。 - Tom Hawtin - tackline
3
像这样的问题让我更想写更多的C#代码... - Daniel Huckstep
显示剩余2条评论
40个回答

19

这是我最近一直在使用的一个:

public static IDisposable Tag(this HtmlHelper html, string tagName)
{
    if (html == null)
        throw new ArgumentNullException("html");

    Action<string> a = tag => html.Write(String.Format(tag, tagName));
    a("<{0}>");
    return new Memento(() => a("</{0}>"));
}

使用方法如下:

using (Html.Tag("ul"))
{
    this.Model.ForEach(item => using(Html.Tag("li")) Html.Write(item));
    using(Html.Tag("li")) Html.Write("new");
}

Memento 是一个方便的类:

public sealed class Memento : IDisposable
{
    private bool Disposed { get; set; }
    private Action Action { get; set; }

    public Memento(Action action)
    {
        if (action == null)
            throw new ArgumentNullException("action");

        Action = action;
    }

    void IDisposable.Dispose()
    {
        if (Disposed)
            throw new ObjectDisposedException("Memento");

        Disposed = true;
        Action();
    }
}

并完成依赖项:

public static void Write(this HtmlHelper html, string content)
{
    if (html == null)
        throw new ArgumentNullException("html");

    html.ViewContext.HttpContext.Response.Write(content);
}

这很好!(不知道为什么有人给它投了反对票)也许对于那些还没有接触过ASP.NET MVC的人来说,这个例子可能没有意义 :) - dso
这里有一个不错的模式。谢谢。+1 - Drew Noakes
我喜欢这个!绝对会放进我的工具箱! - jrista
11
我喜欢这个想法,但我不确定为什么Memento从其Dispose方法中抛出异常。根据MSDN的说法:“为确保资源总是得到适当的清理,应该能够多次调用Dispose方法而不会引发异常。”http://bit.ly/NV3AH - Joel Mueller
1
使用Dispose模式处理非资源相关的问题是一个经常争论的问题,双方都没有明显的胜者。话虽如此,抛出异常仍然是一个不好的主意,因为你无法控制Dispose方法将被调用多少次。想象一下,如果有人在using语句中调用Memento并显式调用Dispose,你最终会得到两个Dispose调用——一个是用户显式调用的,另一个是编译器扩展using语句时添加的。更好的选择可能是使用Assert。 - Scott Dorman
显示剩余2条评论

18
完整的解决方案太大无法在此展示,但我编写了一系列扩展方法,可以轻松将 DataTable 转换为 CSV。
public static String ToCSV(this DataTable dataTable)
{
    return dataTable.ToCSV(null, COMMA, true);
}  

public static String ToCSV(this DataTable dataTable, String qualifier)
{
    return dataTable.ToCSV(qualifier, COMMA, true);
}

private static String ToCSV(this DataTable dataTable, String qualifier, String delimiter, Boolean includeColumnNames)
{
    if (dataTable == null) return null;

    if (qualifier == delimiter)
    {
        throw new InvalidOperationException(
            "The qualifier and the delimiter are identical. This will cause the CSV to have collisions that might result in data being parsed incorrectly by another program.");
    }

    var sbCSV = new StringBuilder();

    var delimiterToUse = delimiter ?? COMMA;

    if (includeColumnNames) 
        sbCSV.AppendLine(dataTable.Columns.GetHeaderLine(qualifier, delimiterToUse));

    foreach (DataRow row in dataTable.Rows)
    {
        sbCSV.AppendLine(row.ToCSVLine(qualifier, delimiterToUse));
    }

    return sbCSV.Length > 0 ? sbCSV.ToString() : null;
}

private static String ToCSVLine(this DataRow dataRow, String qualifier, String delimiter)
{
    var colCount = dataRow.Table.Columns.Count;
    var rowValues = new String[colCount];

    for (var i = 0; i < colCount; i++)
    {
        rowValues[i] = dataRow[i].Qualify(qualifier);
    }

    return String.Join(delimiter, rowValues);
}

private static String GetHeaderLine(this DataColumnCollection columns, String qualifier, String delimiter)
{
    var colCount = columns.Count;
    var colNames = new String[colCount];

    for (var i = 0; i < colCount; i++)
    {
        colNames[i] = columns[i].ColumnName.Qualify(qualifier);
    }

    return String.Join(delimiter, colNames);
}

private static String Qualify(this Object target, String qualifier)
{
    return qualifier + target + qualifier;
}

最终,你可以这样称呼它:

someDataTable.ToCSV(); //Plain old CSV
someDataTable.ToCSV("\""); //Double quote qualifier
someDataTable.ToCSV("\"", "\t"); //Tab delimited

我实际上编写了一整套扩展方法作为重载,允许您传入自定义类来定义格式、列名等等。这些内容在本篇文章中包含进去会显得太多了。 - Josh

13

我不喜欢INotifyPropertyChanged接口要求将属性名称作为字符串传递。我希望有一种强类型的方法,在编译时检查我只为存在的属性提高和处理属性更改。我使用以下代码来实现:

public static class INotifyPropertyChangedExtensions
{
    public static string ToPropertyName<T>(this Expression<Func<T>> @this)
    {
        var @return = string.Empty;
        if (@this != null)
        {
            var memberExpression = @this.Body as MemberExpression;
            if (memberExpression != null)
            {
                @return = memberExpression.Member.Name;
            }
        }
        return @return;
    }
}

在实现INotifyPropertyChanged的类中,我会包含这个辅助方法:
protected void NotifySetProperty<T>(ref T field, T value,
    Expression<Func<T>> propertyExpression)
{
    if (field == null ? value != null : !field.Equals(value))
    {
        field = value;
        this.NotifyPropertyChanged(propertyExpression.ToPropertyName());
    }
}

最终,我可以做到这样的事情:

private string _name;
public string Name
{
    get { return _name; }
    set { this.NotifySetProperty(ref _name, value, () => this.Name); }
}

它是强类型的,我只为实际更改值的属性引发事件。


我喜欢使用变量,例如 private static readonly PropertyChangedEventArgs _namePropertyChangedEventArgs = new PropertyChangedEventArgs("Name"); 并将它们传递给 OnPropertyChanged。相比于字符串(需要查找所有的引用),这样做更方便跟踪;而且节省内存开销,因为每个类型只会创建一次,而不是每个实例每次更改都要创建。 - Sam Harwell
嗨280Z28,也许结合多种技术会更有用,因为你的方法仍然需要更新字符串字面量。实际上,我发现我使用的方法很好,特别是因为属性更改通常是用户生成的,因此缓慢而不频繁。不过我还要检查一下,以确保我没有创建内存泄漏。谢谢。 - Enigmativity

12

虽然这不是很聪明的做法,但我修改了----OrDefault方法,以便您可以在行内指定默认项,而无需在代码中稍后检查null:

    public static T SingleOrDefault<T> ( this IEnumerable<T> source, 
                                    Func<T, bool> action, T theDefault )
    {
        T item = source.SingleOrDefault<T>(action);

        if (item != null)
            return item;

        return theDefault;
    }

这是一种非常简单但确实有助于清理空检查的方法。最好在您的UI期望X项列表(例如锦标赛系统或游戏玩家插槽)并且您想要显示“空座位”时使用。

用法:

    return jediList.SingleOrDefault( 
                 j => j.LightsaberColor == "Orange", 
               new Jedi() { LightsaberColor = "Orange", Name = "DarthNobody");

不错,我一直在想为什么没有为SingleOrDefault和FirstOrDefault包含那个重载,很好的举措! - rmoore
我做了类似的东西,不过我使用了 Func<T> 作为默认值,而不是 T。 - Svish
3
请注意,对于非可空类型,此方法不起作用。内置的SingleOrDefault扩展在未找到任何项时返回default(T),对于引用类型或可空值类型,它将为null。 - LukeH
例如,当T为int且未找到匹配项时,您的方法将始终返回0,无论“theDefault”参数的值是什么。 - LukeH
@Svish,是的,我也写了那个签名,但我发布了这个因为它更容易理解。@Luke,是的,我意识到这一点,但到目前为止我只用它来处理域对象集合。 - John Farrell
5
个人认为它并没有比 .SingleOrDefault() ?? new Foo() 更加清晰。 - Johannes Rudolph

12

我喜欢使用的两个方法是我编写的InsertWhere<T>和RemoveWhere<T>扩展方法。在WPF和Silverlight中使用ObservableCollections时,我经常需要修改有序列表而不必重新创建它们。这些方法允许我根据提供的Func进行插入和删除,因此不需要重新调用.OrderBy()。

    /// <summary>
    /// Removes all items from the provided <paramref name="list"/> that match the<paramref name="predicate"/> expression.
    /// </summary>
    /// <typeparam name="T">The class type of the list items.</typeparam>
    /// <param name="list">The list to remove items from.</param>
    /// <param name="predicate">The predicate expression to test against.</param>
    public static void RemoveWhere<T>(this IList<T> list, Func<T, bool> predicate)
    {
        T[] copy = new T[] { };
        Array.Resize(ref copy, list.Count);
        list.CopyTo(copy, 0);

        for (int i = copy.Length - 1; i >= 0; i--)
        {
            if (predicate(copy[i]))
            {
                list.RemoveAt(i);
            }
        }
    }

    /// <summary>
    /// Inserts an Item into a list at the first place that the <paramref name="predicate"/> expression fails.  If it is true in all cases, then the item is appended to the end of the list.
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="list"></param>
    /// <param name="obj"></param>
    /// <param name="predicate">The sepcified function that determines when the <paramref name="obj"/> should be added. </param>
    public static void InsertWhere<T>(this IList<T> list, T obj, Func<T, bool> predicate)
    {
        for (int i = 0; i < list.Count; i++)
        { 
            // When the function first fails it inserts the obj paramiter. 
            // For example, in a list myList of ordered Int32's {1,2,3,4,5,10,12}
            // Calling myList.InsertWhere( 8, x => 8 > x) inserts 8 once the list item becomes greater then or equal to it.
            if(!predicate(list[i]))
            {
                list.Insert(i, obj);
                return;
            }
        }

        list.Add(obj);
    }

编辑:
Talljoe 对我匆忙构建的 RemoveWhere/RemoveAll 进行了一些重大改进。新版本只需约 50 毫秒(如果可以调用 List.RemoveAll,则不到 10 毫秒),即可删除约 300 万项中的每三个,而 RemoveWhere 则需要多秒钟(我等得有点累了)。以下是他极大地改进后的版本,再次感谢!

    public static void RemoveAll<T>(this IList<T> instance, Predicate<T> predicate)
    {
        if (instance == null)
            throw new ArgumentNullException("instance");
        if (predicate == null)
            throw new ArgumentNullException("predicate");
        if (instance is T[])
            throw new NotSupportedException();

        var list = instance as List<T>;
        if (list != null)
        {
            list.RemoveAll(predicate);
            return;
        }

        int writeIndex = 0;
        for (int readIndex = 0; readIndex < instance.Count; readIndex++)
        {
            var item = instance[readIndex];
            if (predicate(item)) continue;

            if (readIndex != writeIndex)
            {
                instance[writeIndex] = item;
            }
            ++writeIndex;
        }

        if (writeIndex != instance.Count)
        {
            for (int deleteIndex = instance.Count - 1; deleteIndex >= writeIndex; --deleteIndex)
            {
                instance.RemoveAt(deleteIndex);
            }
        }
    }

3
由于在移除项时需要进行所有内存的移位(除非 IList 是一个 LinkedList),因此 RemoveWhere 有些低效。我已经创建了一个修改过的版本:http://pastebin.com/f20e73b4e。不同之处是:1)改名为“RemoveAll”,以匹配 List<T> 的版本。2)如果适用,调用 List<T> 的版本(甚至比我的版本3更有效)。3)使用两个索引遍历列表,并对值进行原地覆盖。4)处理当某人传递一个数组时的情况(我认为实际上更喜欢抛出异常,但我希望在修改数组之前这样做--此处留给读者练习)。 - Talljoe
我甚至没有注意到List<T>.RemoveAll,我只是假设所有的扩展都是扩展IList<T>,而它并没有这个方法。谢谢你指出来!不幸的是,我不能使用它,因为ObservableCollection只是从IList继承而来。值得注意的一点是,你对第二点的检查会导致堆栈溢出,Func<T, bool>转换为Predicate<T>无法转换为Predicate。你的后半部分比我的快得多,我肯定会实现它。如果你不介意,我会用更新的版本编辑我的帖子。 - rmoore
请继续。关于无限递归的问题很有趣,我很确定我测试过它并且它可以工作。这将使其正常工作:list.RemoveAll(t => predicate(t)); - Talljoe

11

这是我随便写的一个方法,所以请随意挑剔。它需要一个有序整数列表,并返回连续区间的字符串列表。例如:

1,2,3,7,10,11,12  -->  "1-3","7","10-12"

这个静态类中的函数:

public static IEnumerable<string> IntRanges(this IEnumerable<int> numbers)
{
    int rangeStart = 0;
    int previous = 0;

    if (!numbers.Any())
        yield break;

    rangeStart = previous = numbers.FirstOrDefault();

    foreach (int n in numbers.Skip(1))
    {
        if (n - previous > 1) // sequence break - yield a sequence
        {
            if (previous > rangeStart)
            {
                yield return string.Format("{0}-{1}", rangeStart, previous);
            }
            else
            {
                yield return rangeStart.ToString();
            }
            rangeStart = n;
        }
        previous = n;
    }

    if (previous > rangeStart)
    {
        yield return string.Format("{0}-{1}", rangeStart, previous);
    }
    else
    {
        yield return rangeStart.ToString();
    }
}

使用示例:

this.WeekDescription = string.Join(",", from.WeekPattern.WeekPatternToInts().IntRanges().ToArray());

这段代码用于将来自一款值得上 DailyWTF 的课程表应用程序的数据进行转换。WeekPattern 是存储在字符串中的比特掩码 "0011011100..."。WeekPatternToInts() 将其转换为 IEnumerable<int>,在本例中为 [3,4,6,7,8],最终变成了 "3-4,6-8"。它提供了一个简洁的描述,说明一门课程在哪些学术周范围内开设。


我暂时想不到这个的立即用途,但是它仍然很棒 :) - Josh
谢谢!我已经添加了一个示例,展示了我如何使用它。 - geofftnz
为什么要执行不必要的操作,比如将 previous 赋值为 n、在每次迭代中检查 first 和格式化单个数字? - okutane
修复了Dmitriy指出的问题(谢谢!) - geofftnz
1
你也有反向的吗? - Svish
@Svish:不好意思,我没有,这只是为了展示目的而已。如果我有空闲时间,我会试一试的。 - geofftnz

11

我有各种.Debugify扩展方法,可以将对象转储到日志文件中。例如,这是我的Dictionary debugify(我也有针对List、Datatable、param array等的扩展方法):

public static string Debugify<TKey, TValue>(this Dictionary<TKey, TValue> dictionary) {
    string Result = "";

    if (dictionary.Count > 0) {
        StringBuilder ResultBuilder = new StringBuilder();

        int Counter = 0;
        foreach (KeyValuePair<TKey, TValue> Entry in dictionary) {
            Counter++;
            ResultBuilder.AppendFormat("{0}: {1}, ", Entry.Key, Entry.Value);
            if (Counter % 10 == 0) ResultBuilder.AppendLine();
        }
        Result = ResultBuilder.ToString();
    }
    return Result;
}

这里有一个用于 DbParameterCollection 的示例(可以用于将数据库调用转储到日志文件中):

public static string Debugify(this DbParameterCollection parameters) {
    List<string> ParameterValuesList = new List<string>();

    foreach (DbParameter Parameter in parameters) {
        string ParameterName, ParameterValue;
        ParameterName = Parameter.ParameterName;

        if (Parameter.Direction == ParameterDirection.ReturnValue)
            continue;

        if (Parameter.Value == null || Parameter.Value.Equals(DBNull.Value))
            ParameterValue = "NULL";
        else
        {
            switch (Parameter.DbType)
            {
                case DbType.String:
                case DbType.Date:
                case DbType.DateTime:
                case DbType.Guid:
                case DbType.Xml:
                    ParameterValue
                        = "'" + Parameter
                                .Value
                                .ToString()
                                .Replace(Environment.NewLine, "")
                                .Left(80, "...") + "'"; // Left... is another nice one
                    break;

                default:
                    ParameterValue = Parameter.Value.ToString();
                    break;
            }

            if (Parameter.Direction != ParameterDirection.Input)
                ParameterValue += " " + Parameter.Direction.ToString();
        }

        ParameterValuesList.Add(string.Format("{0}={1}", ParameterName, ParameterValue));
    }

    return string.Join(", ", ParameterValuesList.ToArray());
}

示例结果:

Log.DebugFormat("EXEC {0} {1}", procName, params.Debugify);
// EXEC spProcedure @intID=5, @nvName='Michael Haren', @intRefID=11 OUTPUT

请注意,如果您在数据库调用之后调用此函数,则输出参数也将被填充。我在包括存储过程名称的行上调用此函数,以便可以将调用复制/粘贴到 SSMS 进行调试。


这些使我的日志文件更加美观,易于生成,而不会中断我的代码。


9

一对将基于36进制的字符串转换为整数的扩展方法:

public static int ToBase10(this string base36)
{
    if (string.IsNullOrEmpty(base36))
        return 0;
    int value = 0;
    foreach (var c in base36.Trim())
    {
        value = value * 36 + c.ToBase10();
    }
    return value;
}

public static int ToBase10(this char c)
{
    if (c >= '0' && c <= '9')
        return c - '0';
    c = char.ToUpper(c);
    if (c >= 'A' && c <= 'Z')
        return c - 'A' + 10;
    return 0;
}

有些天才决定将数字编码为字符串存储在数据库中,因为十进制数占用太多空间。十六进制更好,但不使用字符G-Z。所以显然需要将基数16扩展到基数36!


2
嗯,需要一个导入功能,可以将您的示例直接发布到每日WTF上。(尽管我最近与一位客户进行了咨询,他们正在创建并使用自定义GUID来管理数据库。创建GUID,截断末尾并附加时间戳。真的很让人费解。) - rmoore
我曾使用过36进制数字,但只有在需要支持一个大的地址空间以容纳可人类阅读标识符的情况下才使用,并且需要将其适应于古老(且无法更改)系统消息格式中的4字节字段。虽然真正的解决方案是抛弃古老的系统,但我仍认为这是一种可辩解的用例。 - Robert Rossney
总有base64 :) 我不确定我所处理的应用程序中的决策过程是什么。我被告知该应用实际上使用平面文件来存储数据,并且为了使它成为多用户系统,他们只需在保存时将所有内容传输到数据库中,并在初始化时刷新本地文件存储。 - geofftnz

7
我编写了一系列扩展方法,以便更轻松地操作ADO.NET对象和方法:

只需一个指令即可从DbConnection创建DbCommand:

    public static DbCommand CreateCommand(this DbConnection connection, string commandText)
    {
        DbCommand command = connection.CreateCommand();
        command.CommandText = commandText;
        return command;
    }

向 DbCommand 添加参数:

    public static DbParameter AddParameter(this DbCommand command, string name, DbType dbType)
    {
        DbParameter p = AddParameter(command, name, dbType, 0, ParameterDirection.Input);
        return p;
    }

    public static DbParameter AddParameter(this DbCommand command, string name, DbType dbType, object value)
    {
        DbParameter p = AddParameter(command, name, dbType, 0, ParameterDirection.Input);
        p.Value = value;
        return p;
    }

    public static DbParameter AddParameter(this DbCommand command, string name, DbType dbType, int size)
    {
        return AddParameter(command, name, dbType, size, ParameterDirection.Input);
    }

    public static DbParameter AddParameter(this DbCommand command, string name, DbType dbType, int size, ParameterDirection direction)
    {
        DbParameter parameter = command.CreateParameter();
        parameter.ParameterName = name;
        parameter.DbType = dbType;
        parameter.Direction = direction;
        parameter.Size = size;
        command.Parameters.Add(parameter);
        return parameter;
    }

通过名称而不是索引访问 DbDataReader 字段:

    public static DateTime GetDateTime(this DbDataReader reader, string name)
    {
        int i = reader.GetOrdinal(name);
        return reader.GetDateTime(i);
    }

    public static decimal GetDecimal(this DbDataReader reader, string name)
    {
        int i = reader.GetOrdinal(name);
        return reader.GetDecimal(i);
    }

    public static double GetDouble(this DbDataReader reader, string name)
    {
        int i = reader.GetOrdinal(name);
        return reader.GetDouble(i);
    }

    public static string GetString(this DbDataReader reader, string name)
    {
        int i = reader.GetOrdinal(name);
        return reader.GetString(i);
    }

    ...

还有一个(与此无关的)扩展方法,允许我在WinForms窗体和控件上执行DragMove操作(就像在WPF中),请看这里


5
这是一种扩展方法,它可以在引发事件之前集中进行 null 检查。
public static class EventExtension
{
    public static void RaiseEvent<T>(this EventHandler<T> handler, object obj, T args) where T : EventArgs
    {
        EventHandler<T> theHandler = handler;

        if (theHandler != null)
        {
            theHandler(obj, args);
        }
    }
}

+1。但在检查 null 值之前,你应该先将事件处理程序的实例复制到一个本地变量中(事件处理程序实例是不可变的)。这可以防止多线程环境中的一些潜在竞争条件(另一个线程可能已在空值检查之后但调用处理程序之前取消订阅)。 - Yann Trevin
没错。我在自己的代码中也这么做了,但从未回来更改过这个答案。我编辑了我的回答。 - Taylor Leese
1
@Yann Trevin:这是不必要的,因为当传递给扩展方法作为参数时,EventHandler<T>对象已经被复制一次,所以不存在竞态条件。 - ShdNx

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接