Почему встраивание метода не эквивалентно объявлению его явно? - PullRequest
3 голосов
/ 02 октября 2009

У вас есть слепое пятно в программировании?

Я имею в виду, есть ли общая техника или языковая функция, к которой вы не можете привыкнуть. Ну, у меня есть один (или, возможно, более одного), и мое использование delegate. Руки вверх! Кто еще не чувствует себя комфортно с делегатами? Будьте честны!

Так, что делегат?

Поскольку мои курсы в университете познакомили меня с Си, я знаю о функциональных указателях. Указатели на функции удобны, если вы хотите передать методы в качестве аргументов. На мой взгляд, делегат - это что-то вроде указателя на функцию. Эврика! Я понял. У меня нет!

Конкретный сценарий?

Я хотел бы удалить любую строку из текстового файла, которая соответствует регулярному выражению . Предполагая, что у меня есть коллекция строк, 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 и изучу лямбды.

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) помогает учиться.

Ответы [ 7 ]

8 голосов
/ 02 октября 2009

Вы пытаетесь использовать метод с подписью:

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 ... это другой вопрос.)

2 голосов
/ 02 октября 2009

Чтобы расширить некоторые другие ответы здесь, вот общая функция карри для 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);
}

Тогда вы будете использовать каррирование для исправления первого параметра и получения делегата, который затем сможете преобразовать в предикат, например:

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());
2 голосов
/ 02 октября 2009

По сути, ваш анонимный делегат заставляет компилятор делать следующее: генерировать класс с непроизносимым именем, имеющим поле 'pattern' и метод, подобный тому, который вы написали в делегате. Сгенерированный класс выглядит так:

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. Конечно, все упомянутое можно сделать вручную, это просто более простой способ сделать это.

Надеюсь, это поможет.

1 голос
/ 19 октября 2009

Что вас укусило, так это то, что программисты на С обычно не рассматривают функции с разными аргументами как разные по типу - им не приходит в голову передать указатель на функцию с двумя строковыми аргументами, где ожидается, что указатель на функцию с одним строковым аргументом должен генерировать ошибку типа во время компиляции, как, например, Алгол 68.

В этом виноват только язык C: он может фактически правильно указывать указатели функций по аргументам и возвращать типы. Но обозначения для этих типов действительно неуклюжи, компиляторы C не всегда требуют этого, и когда они делают это, программисты стремятся обойти это, приводя все указатели к (void *) в любом случае.

Изучение C как первого языка научит вас некоторым вредным привычкам.

1 голос
/ 02 октября 2009

В C # 2.0 вы можете создать анонимного делегата, который вы можете использовать для захвата вашей переменной шаблона:

        int result = lines.RemoveAll( delegate (string s) {return DoesLineMatch(s, pattern);});
0 голосов
/ 02 октября 2009

Вы можете объявить это так:

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

Где pattern - приватная переменная в вашем классе. Но это немного уродливо, поэтому вы можете объявить удаление встроенным и использовать закрытие для переменной шаблона, которая объявлена ​​локально в вашем методе RemoveLinesFromFile.

0 голосов
/ 02 октября 2009

Вот говорит Джон.

Кроме того, в C # 3 вы можете использовать лямбду, если вы все еще хотите передать pattern в ваш метод:

int result = lines.RemoveAll(l => DoesLineMatch(l, pattern));
...