Ошибка в .net Regex.Replace?

Следующий код...

using System;
using System.Text.RegularExpressions;

public class Program
{
    public static void Main()
    {
        var r = new Regex("(.*)");
        var c = "XYZ";
        var uc = r.Replace(c, "A $1 B");

        Console.WriteLine(uc);
    }
}

. Ссылка Net Fiddle

выводит следующий результат...

A XYZ BA B

Считаете ли вы, что это правильно?

Не должно быть выход...

A XYZ B

Думаю, я делаю здесь что-то глупое. Я был бы признателен за любую помощь, которую вы можете предоставить, помогая мне понять эту проблему.


Вот что-то интересное...

using System;
using System.Text.RegularExpressions;

public class Program
{
    public static void Main()
    {
        var r = new Regex("(.*)");
        var c = "XYZ";
        var uc = r.Replace(c, "$1");

        Console.WriteLine(uc);
    }
}

.NET Fiddle

Вывод...

XYZ

Ответ 1

Что касается того, почему движок возвращает 2 совпадения, это связано с тем, как .NET(также Perl и Java) обрабатывает глобальное соответствие, т.е. находит все совпадения с данным шаблоном во входной строке.

Процесс можно описать следующим образом (текущий индекс обычно устанавливается в 0 в начале поиска, если не указано):

  • Из текущего индекса выполните поиск.
  • Если нет совпадения:
    • Если текущий индекс уже указывает в конце строки (текущий индекs >= строка .length), верните результат до сих пор.
    • Инкремент текущего индекса на 1, перейдите к шагу 1.
  • Если основное совпадение ($0) не пусто (по крайней мере один символ потребляется), добавьте результат и установите текущий индекс в конец основного соответствия ($0). Затем перейдите к шагу 1.
  • Если главное совпадение ($0) пусто:
    • Если предыдущее совпадение не пустое, добавьте результат и перейдите к шагу 1.
    • Если предыдущее совпадение пустое, назад и продолжить поиск.
    • Если попытка обратного отслеживания находит непустое совпадение, добавьте результат, установите текущий индекс в конец совпадения и перейдите к шагу 1.
    • В противном случае увеличивайте текущий индекс на 1. Перейдите к шагу 1.

Двигатель должен проверить наличие пустого совпадения; в противном случае это закончится бесконечным циклом. Дизайнер признает использование пустого совпадения (например, при разбиении строки на символы), поэтому двигатель должен быть сконструирован таким образом, чтобы избежать застревания в определенной позиции навсегда.

Этот процесс объясняет, почему в конце есть пустое совпадение: поскольку поиск ведется в конце строки (индекс 3) после (.*) соответствует abc, а (.*) может соответствовать пустой строке, найдено пустое совпадение. И движок не создает бесконечное количество пустых совпадений, так как в конце уже найдено пустое совпадение.

 a b c
^ ^ ^ ^
0 1 2 3

Первое совпадение:

 a b c
^     ^
0-----3

Второе совпадение:

 a b c
      ^
      3

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

Обратите внимание, что JavaScript просто увеличивает текущий индекс на 1, если основное совпадение пуст, поэтому не более 1 соответствует индексу. Однако в этом случае (.*), если вы используете глобальный флаг g для глобального сопоставления, тот же результат произойдет:

(Результат ниже из Firefox, обратите внимание на флаг g)

> "XYZ".replace(/(.*)/g, "A $1 B")
"A XYZ BA  B"

Ответ 2

Мне нужно подумать, почему это происходит. Уверен, что тебе что-то не хватает. Хотя это исправить проблему. Просто привяжите регулярное выражение.

var r = new Regex("^(.*)$");

Здесь . Демо-версия NetFiddle

Ответ 3

У вас регулярное выражение имеет два совпадения, и Replace заменит их оба. Первый - "XYZ", а второй - пустая строка. Я не уверен, почему у него есть два матча в первую очередь. Вы можете исправить его с помощью ^ (. *) $, Чтобы заставить его рассмотреть начало и конец строки.

Или используйте + вместо *, чтобы он соответствовал хотя бы одному символу.

.* соответствует пустой строке, поскольку она имеет нулевые символы.

.+ не соответствует пустой строке, потому что требуется хотя бы один символ.

Интересно, что в Javascript (в Chrome):

var r = /(.*)/;
var s = "XYZ";
console.log(s.replace(r,"A $1 B");

Выведет ожидаемый A XYZ B без ложного дополнительного соответствия.

Изменить (спасибо @nhahtdh): добавив флаг g в регулярное выражение Javascript, дайте тот же результат, что и в .NET:

var r = /(.*)/g;
var s = "XYZ";
console.log(s.replace(r,"A $1 B");

Ответ 4

Коэффициент * соответствует 0 или более. Это приводит к 2-мя совпадениям. XYZ и ничего.

Попробуйте использовать квантор +, который соответствует 1 или более.

Простое объяснение заключается в том, чтобы посмотреть на строку следующим образом: XYZ<nothing>

  • У нас есть совпадения XYZ и <nothing>
  • Для каждого матча
    • Матч 1: Заменить XYZ на A $1 B ($ 1 здесь XYZ) Результат: A XYZ B
    • Матч 2: Заменить <nothing> на A $1 B ($ 1 здесь <nothing>) Результат: A B

Конечный результат: A XYZ BA B

Почему <nothing> является совпадением сам по себе интересен и что-то, о чем я действительно не думал. (Почему нет бесконечных <nothing> совпадений?)

Ответ 5

Regex - своеобразный язык. Вы должны точно понимать, что (. *) Будет соответствовать. Вам также нужно понимать жадность.

  • (. *) будет жадно соответствовать 0 или более символам. Итак, в строке "XYZ" она будет соответствовать всей строке с ее первым совпадением и поместить ее в позицию $1, давая вам следующее:

    A XYZ B Затем он будет продолжать пытаться сопоставить и сопоставить null в конце строки, установив ваш $1 в null, давая вам следующее:

    A B Результат в строке, которую вы видите:

    A XYZ BA B

  • Если вы хотите ограничить жадность и сопоставить каждый символ, вы должны использовать это выражение:

    (. *?)
    Это будет соответствовать каждому символу X, Y и Z отдельно, а также null в конце и приведет к следующему:

    A BXA BYA BZA B

Если вы не хотите, чтобы ваше регулярное выражение превышало границы вашей строки, ограничьте регулярное выражение идентификаторами ^ и $.

Чтобы дать вам более точную информацию о том, что происходит, рассмотрите этот тест и результирующие сопоставимые группы.

    [TestMethod()]
    public void TestMethod3()
    {
        var myText = "XYZ";
        var regex = new Regex("(.*)");
        var m = regex.Match(myText);
        var matchCount = 0;
        while (m.Success)
        {
            Console.WriteLine("Match" + (++matchCount));
            for (int i = 1; i <= 2; i++)
            {
                Group g = m.Groups[i];
                Console.WriteLine("Group" + i + "='" + g + "'");
                CaptureCollection cc = g.Captures;
                for (int j = 0; j < cc.Count; j++)
                {
                    Capture c = cc[j];
                    Console.WriteLine("Capture" + j + "='" + c + "', Position=" + c.Index);
                }
            }
            m = m.NextMatch();
        }

Вывод:

Match1
Group1='XYZ'
Capture0='XYZ', Position=0
Group2=''
Match2
Group1=''
Capture0='', Position=3
Group2=''

Обратите внимание, что есть две группы, которые соответствуют. Первой была вся группа XYZ, а вторая - пустая. Тем не менее, были две группы. Таким образом, $1 был заменен на XYZ в первом случае и null для второго.

Также обратите внимание, что передняя косая черта / - это еще один символ, который рассматривается в двигателе regex.net и не имеет особого значения. Парсер javascript обрабатывает / иначе, потому что он должен потому, что он существует в рамках парсеров HTML, где особое внимание уделяется </.

Наконец, чтобы получить то, что вы действительно хотите, рассмотрите этот тест:

    [TestMethod]
    public void TestMethod1()
    {
        var r = new Regex(@"^(.*)$");
        var c = "XYZ";
        var uc = r.Replace(c, "A $1 B");

        Assert.AreEqual("A XYZ B", uc);
    }