C# 递归方法的意外结果

Posted

技术标签:

【中文标题】C# 递归方法的意外结果【英文标题】:Unexpected results with C# recursion method 【发布时间】:2010-11-10 12:54:11 【问题描述】:

我有一个相当简单的方法,它递归地删除开始/结束 html 标记

class Program
       
        static void Main(string[] args)
        
            string s = FixHtml("<div><p>this is a <strong>test</strong></p></div>");
            Console.WriteLine(s);
        

        private static string FixHtml(string s)
                    
            //Remove any outer <div>
            if (s.ToLower().StartsWith("<div>"))
            
                FixHtml(s.Substring(5, s.Length - 5));
            
            else if (s.ToLower().StartsWith("<p>"))
            
                FixHtml(s.Substring(3, s.Length - 3));
            
            else if (s.ToLower().EndsWith("</div>"))
            
                FixHtml(s.Substring(0, s.Length - 6));
            
            else if (s.ToLower().EndsWith("</p>"))
            
                FixHtml(s.Substring(0, s.Length - 4));
            

            return s;
        
    

行为是它可以递归删除&lt;div&gt; &amp; &lt;p&gt;标签,但是在“return s”语句中它撤消了所有的工作,通过添加回添加标签!

有人知道为什么会这样吗?以及如何强制它返回我想要的值。即this is a &lt;strong&gt;test&lt;/strong&gt;

【问题讨论】:

【参考方案1】:

在 .NET 中,字符串是不可变的 - 因此您的方法实际上永远不会更改返回值。当您调用s.ToLower().StartsWith("&lt;div&gt;") 时,您会得到一个具有预期差异的新字符串现有字符串 s 保持不变。

此外,您永远不会消耗递归调用的返回值。

别想了,试试这样的:

    private static string FixHtml(string s)
                
        if (s.ToLower().StartsWith("<div>"))
        
            return FixHtml(s.Substring(5, s.Length - 5));
        
        else if (s.ToLower().StartsWith("<p>"))
        
            return FixHtml(s.Substring(3, s.Length - 3));
        
        else if (s.ToLower().EndsWith("</div>"))
        
            return FixHtml(s.Substring(0, s.Length - 6));
        
        else if (s.ToLower().EndsWith("</p>"))
        
            return FixHtml(s.Substring(0, s.Length - 4));
        

        return s;
    

【讨论】:

或者至少将每个FixHtml调用的返回字符串赋值给变量s。 @mkmurray - 是的,这行得通。对于那些属于“单一出口点”阵营的人来说,这是一个不错的选择。【参考方案2】:

请注意,原始文本操作通常是处理 xml 的一种糟糕方式 - 例如,您目前没有处理属性、命名空间、尾随标签空格 (&lt;p &gt;) 等。

通常,我会说将其加载到 DOM 中(XmlDocument/XDocument 用于 xhtml;HTML Agaility Pack 用于 html) - 但实际上我想知道在这种情况下 xslt 是否会很好......

例如:

static void Main()

    string xhtml = @"<div><p>this is a <strong>test</strong></p></div>";
    XslCompiledTransform xslt = new XslCompiledTransform();
    xslt.Load("strip.xslt");

    StringWriter sw = new StringWriter();
    using(XmlReader xr = XmlReader.Create(new StringReader(xhtml))) 
        xslt.Transform(xr, null, sw);
    
    string newHtml = sw.ToString();
    Console.WriteLine(newHtml);

使用 strip.xslt:

<?xml version="1.0" encoding="utf-8"?>
<xsl:stylesheet version="1.0" xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
  <xsl:output method="xml" indent="no" omit-xml-declaration="yes"/>
  <xsl:template match="strong|@*">
    <xsl:copy><xsl:apply-templates select="*|text()"/></xsl:copy>
  </xsl:template>
  <xsl:template match="*">
    <xsl:apply-templates select="*|text()"/>
  </xsl:template>
</xsl:stylesheet>

【讨论】:

XSLt 听起来是一个非常适合解决这个问题的工具。虽然写起来没那么有趣……【参考方案3】: 您没有对嵌套调用返回的字符串做任何事情。 更改字符串时,会创建一个新对象,而不是更改现有对象(它们是不可变的)。 如果您想在不使用返回值的情况下采用类似的方法,您可以将字符串参数设为“ref”参数。尽管其他人提到的性能下降仍然适用。

【讨论】:

【参考方案4】:

您需要像这样为每个 FixHtml 调用添加一个返回值:

   private static string FixHtml(string s)
                
        //Remove any outer <div>
        if (s.ToLower().StartsWith("<div>"))
        
            return FixHtml(s.Substring(5, s.Length - 5));
        
        else if (s.ToLower().StartsWith("<p>"))
        
            return FixHtml(s.Substring(3, s.Length - 3));
        
        else if (s.ToLower().EndsWith("</div>"))
        
            return FixHtml(s.Substring(0, s.Length - 6));
        
        else if (s.ToLower().EndsWith("</p>"))
        
            return FixHtml(s.Substring(0, s.Length - 4));
        

        return s;
    

【讨论】:

【参考方案5】:

您需要使用StringBuilder 才能工作,或者在每次调用FixHTML 时复制字符串才能工作。这是因为字符串在 .NET 中是不可变的。

你可以look here看看什么是不可变字符串。

【讨论】:

【参考方案6】:

如果您打算在服务器上执行此操作,则必须使用字符串生成器。原因是如果您使用字符串,内存性能将非常糟糕。每次您从字符串中去除标签时,您实际上都会有效地复制字符串。对于每个递归(标记),您的系统都会执行此操作,因此即使您有合理大小的 HTML 输入,您也会很快使用大量内存。

编辑:参考 Chris 的评论,如果您正在处理大字符串,则前面的陈述是正确的。如果您使用字符串生成器解析小块 HTML,则不那么重要。但我假设您是在 Web 环境中的服务器上使用它,因此您可能会使用它来消耗非常大的页面。

使用字符串生成器作为参考还允许您的函数操作可变值,因此在递归结束时,StringBuilder.ToString() 将正确输出您的变异字符串。

如果您支持我的解决方案,您应该支持其他提到字符串可变性作为您的问题的人:)。

我试图回答您的问题并解决下一个问题,但我认为这是许多人以前犯过的错误。

另请注意,您的代码会在 &lt;br/&gt; 上死掉

private static string FixHtml(StringBuilder bldr)    
                    
    if (String.Compare(blder.ToString(0,5), "<div>", true) == 0)        
    
        blder.remove(0, 5);            
        return FixHtml(blder);        
           
    else if (String.Compare(blder.ToString(0,3), "<p>", true) == 0)       
    
        blder.remove(0, 3);            
        return FixHtml(blder);             
            
    else if (String.Compare(blder.ToString(bldr.Length - 6, 6), "</div>", true) == 0)       
    
        blder.remove(blder.Length - 6, 6);            
        return FixHtml(blder);                   
            
    else if (String.Compare(blder.ToString(bldr.Length - 4, 4), "</p>", true) == 0)       
            
        blder.remove(blder.Length - 4, 4);            
        return FixHtml(blder); 
           
    return blder.ToString();    

【讨论】:

克里斯,我们有一个说法,一点点知识是危险的。如果他在合理大小的 HTML 块上使用此算法,那么该函数将具有非常非常糟糕的性能,内存压力乘以调用它的客户端数量。如果您使用字符串在一个合理的字符串上对一个函数进行一次变异,您将看不到任何改进。编程是关于内存、性能、正确性和构建结果所需时间之间的折衷。但我很乐意向您展示我没有“错误”的测试用例。 对于小块代码来说,它的价值是什么。我想知道为什么代码会在 上失败? (不在我的开发机器上测试) 看看你的算法。如果您点击 ,您将无法捕捉到它并且移除将停止,即使那里有更多标签。您也无法处理具有 属性的标签 好的,我明白了,我们可以为 或任何需要的标签添加测试。我想解决类和/或 ids 的问题是要有 StartsWith("

以上是关于C# 递归方法的意外结果的主要内容,如果未能解决你的问题,请参考以下文章

c#如何让递归函数输出多个结果

WCF双向调用:意外递归

使用递归属性 ASP.NET / C#

浅析C#中的线性递归与尾递归

为啥我的递归会产生意外错误?以及如何在到达循环时修改递归?

浅析C#中的线性递归与尾递归