3

你有编程盲点吗?

我的意思是有一个你无法真正习惯的通用技术或语言功能。好吧,我有一个(或者可能不止一个),而我的是使用delegate. 举手!还有谁对代表不满意?说实话!

那么什么是委托人?

由于我在大学的课程向我介绍了 C,我知道函数指针。如果您想将方法作为参数传递,函数指针很方便。所以在我看来,委托就像一个函数指针。尤里卡!我得到了它。我没有!

一个具体的场景?

我想从与正则表达式匹配的文本文件中删除任何行。假设我有一系列线条,List<T>有一种RemoveAll似乎非常适合该目的的方法。 RemoveAll期望评估方法作为决定是否删除或离开列表元素的参数。它就是:函数指针!

这里有代码吗?

public static int RemoveLinesFromFile(string path, string pattern)
{
  List<string> lines = new List<string>(File.ReadAllLines(path));
  int result = lines.RemoveAll(DoesLineMatch);
  File.WriteAllLines(path, lines.ToArray());
  return result;
}

所以我正在寻找一个函数DoesLineMatch来评估一行是否匹配一个模式。

你看到问题了吗?

RemoveAll期望一个委托Predicate<string> match作为参数。我会这样编码:

private static bool DoesLineMatch(string line, string pattern)
{
  return Regex.IsMatch(line, pattern);
}

但是后来我收到一个错误“期望一个带有'bool DoesLineMatch(string)'签名的方法”。我在这里想念什么?

它真的有效吗?

这就是我最终让它工作的方式:

public static int RemoveLinesFromFile(string path, string pattern)
{
  List<string> lines = new List<string>(File.ReadAllLines(path));
  int result = lines.RemoveAll(delegate(string line)
    {
      return Regex.IsMatch(line, pattern);
    });
  File.WriteAllLines(path, lines.ToArray());
  return result;
}

我很高兴它有效,但我不明白。

问题是什么?

我所做的只是内联该方法。据我了解内联,它只是某种使用一次并销毁的代码。如果你只使用一次变量或方法,你可以内联它,但内联总是等同于显式声明它。

有没有办法明确声明该方法?我该怎么做?

PS.:请原谅我的问题有点冗长。

PPS.:一旦我得到这个委托,我就会从 2.0 飞跃到 3.0 并学习 lambdas。

PPPS .:按照乔恩关于效率的提示,Regex.IsMatch(string, string)我修改了我的代码:

  int result = lines.RemoveAll(delegate(string line)
    {
      Regex regex = new Regex(pattern);
      return regex.IsMatch(line);
    });

这对效率问题没有多大帮助。所以我按照ReSharper的建议,将 Regex 实例化移到了外部范围:

  Regex regex = new Regex(pattern);
  int result = lines.RemoveAll(delegate(string line)
    {
      return regex.IsMatch(line);
    });

现在 ReSharper 敦促我用方法组替换它:

  Regex regex = new Regex(pattern);
  int result = lines.RemoveAll(regex.IsMatch);

这与此处提出的答案非常相似。不是我要求的,但我再次对 ReSharper(当然还有 Stack Overflow)如何帮助学习感到惊讶。

4

7 回答 7

8

您正在尝试使用具有以下签名的方法:

bool DoesLineMatch(string line, string pattern)

对于有签名的代表:

bool Predicate(string value)

它会从哪里得到第二个字符串值(模式)?

使用显式声明的方法执行此操作的唯一方法是这样的:

public sealed class RegexHolder
{
    private readonly string pattern;

    public RegexHolder(string pattern)
    {
        this.pattern = pattern;
    }

    public bool DoesLineMatch(string line)
    {
        return Regex.IsMatch(line, pattern);
    }
}

然后:

public static int RemoveLinesFromFile(string path, string pattern)
{
    List<string> lines = new List<string>(File.ReadAllLines(path));
    RegexHolder holder = new RegexHolder(pattern);
    int result = lines.RemoveAll(holder.DoesLineMatch);
    File.WriteAllLines(path, lines.ToArray());
    return result;
}

这与编译器使用匿名方法为您所做的很接近——它将创建一个嵌套类来保存捕获的变量pattern在本例中)。

(请注意,我避免讨论调用的效率,Regex.Match(string, string)而不是创建...的单个实例,Regex这是另一回事。)

于 2009-10-02T10:37:09.607 回答
2

基本上,您的匿名委托会导致编译器执行以下操作:生成具有不可发音名称的类,该类具有字段“模式”和类似于您在委托中编写的方法。生成的类如下所示:

class Matcher {
    public string Pattern;
    bool IsMatch(string value){
       return Regex.IsMatch(Pattern, value);
    }
}

你看,这个类将两个参数函数转换为一个有一个参数的函数。

您的代码将转换为类似

public static int RemoveLinesFromFile(string path, string pattern)
{
  List<string> lines = new List<string>(File.ReadAllLines(path));
  Matcher matcher = new Matcher(pattern);
  int result = lines.RemoveAll(matcher.IsMatch);
  File.WriteAllLines(path, lines.ToArray());
  return result;
}

您会看到,运行时从作用域中获取一个变量并将其与函数绑定。现在您有了一个包含附加变量的所需签名的函数。这就是为什么从 CS 的角度来看委托被称为闭包的原因。当然,上面提到的一切都可以手动制作,这只是一种更简单的方法。

希望这可以帮助。

于 2009-10-02T10:52:54.263 回答
2

为了在这里扩展其他一些答案,这里有一个 C# 的通用柯里化函数:

public static class DelegateUtils
{
    public static Predicate<T> ToPredicate<T>(this Func<T, Boolean> func)
    {
        return value => func(value);
    }

    public static Func<TResult> Curry<T1, TResult>(
        this Func<T1, TResult> func, T1 firstValue)
    {
        return () => func(firstValue);
    }

    public static Func<T2, TResult> Curry<T1, T2, TResult>(
        this Func<T1, T2, TResult> func, T1 firstValue)
    {
        return p2 => func(firstValue, p2);
    }

    public static Func<T2, T3, TResult> Curry<T1, T2, T3, TResult>(
        this Func<T1, T2, T3, TResult> func, T1 firstValue)
    {
        return (p2, p3) => func(firstValue, p2, p3);
    }

    // if you need more, follow the examples
}

在您的示例中,您可以将参数的顺序切换到您的匹配函数,以便您要匹配的参数是第一个,如下所示:

private static bool DoesLineMatch(string pattern, string line)
{
    return Regex.IsMatch(line, pattern);
}

然后,您将使用 currying 来修复第一个参数,并获得一个委托,然后您可以将其转换为谓词,如下所示:

Func<String, String, Boolean> func = DoesLineMatch;
Func<String, Boolean> predicateCandidate = func.Curry("yourPattern");
Predicate<String> predicate = predicateCandidate.ToPredicate();
lines.RemoveAll(predicate);

当然,您可以将其全部内联:

lines.RemoveAll(new Func<String, String, Boolean>(DoesLineMatch)
    .Curry("yourPattern")
    .ToPredicate());
于 2009-10-02T12:09:12.160 回答
1

在 C# 2.0 中,您可以创建一个匿名委托,您可以使用它来捕获您的模式变量:

        int result = lines.RemoveAll( delegate (string s) {return DoesLineMatch(s, pattern);});
于 2009-10-02T10:48:45.660 回答
1

您在这里被咬的是这样一种现象,即 C 程序员通常不会将具有不同参数的函数视为不同类型的函数 - 他们不会想到将指针传递给具有两个字符串参数的函数,其中指针指向带有单个字符串参数的函数应该在编译时产生类型错误,就像在 Algol 68 中发生的那样。

C 语言只能部分归咎于此:它实际上可以通过参数和返回类型正确地键入函数指针。但是这些类型的符号真的很尴尬,C 编译器并不总是需要这个,当他们这样做时,程序员倾向于通过将所有指针强制转换为 (void *) 来绕过它。

学习 C 作为第一语言确实会教你一些坏习惯。

于 2009-10-19T17:30:16.657 回答
0

沃特乔恩说。

此外,在 C# 3 中,您可能会选择使用 lambda,假设您仍想传递pattern给您的方法:

int result = lines.RemoveAll(l => DoesLineMatch(l, pattern));
于 2009-10-02T10:41:41.487 回答
0

你可以这样声明:

bool DoesLineMatch(string line)
{
  return Regex.IsMatch(line, pattern);
}

其中 pattern 是您班级中的私有变量。但这有点难看,这就是为什么您可以声明内联委托并对在 RemoveLinesFromFile 方法中本地声明的模式变量使用闭包。

于 2009-10-02T10:41:55.653 回答