3

我有一些像这样的 C# 代码(类文件 = Status.cs):

    /// <summary>
    /// Constructs a status entity with the text specified
    /// </summary>
    /// <param name="someParameter">Some parameter.</param>
    public Status(string someParameter)
    {
        SomeProperty = someParameter;
    }

    /// <summary>
    /// An example of a virtual property.
    /// </summary>
    public virtual string SomeProperty { get; private set; }

我想对它做三件事:

  1. 在其上执行 Resharper “具有支持字段的属性”
  2. 摆脱“私人集”并用常规“集”替换它
  3. 更改构造函数,使其初始化私有字段而不是属性

所以最终的结果应该是这样的:

    /// <summary>
    /// Constructs a status entity with the text specified
    /// </summary>
    /// <param name="someParameter">Some parameter.</param>
    public Status(string someParameter)
    {
        _someProperty = someParameter;
    }

    private string _someProperty;

    /// <summary>
    /// An example of a virtual property.
    /// </summary>
    public virtual string SomeProperty
    {
        get { return _someProperty; }
        set { _someProperty = value; }
    }

我的问题是:有没有办法使用 Resharper API 来自动化这种类型的重构?

背景:

对于那些可能想知道我为什么要这样做的人,这是因为:

  1. 我正在升级 NHibernate (old=2.1.2.4000, new=3.3.1.4000) 和 Fluent NHibernate (old=1.1.0.685, new=1.3.0.0)。

  2. 我已经摆脱了旧的 NHibernate.ByteCode.Castle.dll 和配置文件中的相应行,所以我现在可以使用内置在最新 NHibernate 中的默认代理。

  3. 在 NHibernate 的新实现和 Fluent 的新版本之间,当我尝试构建和运行单元测试时似乎存在两个问题(其中一部分是 FxCop 抱怨,但无论如何):

a)由于“私有集”而引发异常,并且 b)由于正在构造函数中初始化虚拟属性而引发异常。

所以我发现如果我进行这些更改,它会编译并且单元测试通过。

所以这对一两个类来说很好,但是有超过 800 个类文件,谁知道有多少属性。

我确信有很多方法可以做到这一点(例如,使用反射、通过目录递归和解析文件等),但似乎 Resharper 是这样的合适工具。

任何帮助表示赞赏,谢谢,-Dave

- 回答“只需将其更改为受保护的集合就完成了”的答案:

不幸的是,事情并没有那么简单。

这是运行单元测试时发生的错误(在对任何类进行任何更改之前):

测试方法抛出异常:NHibernate.InvalidProxyTypeException:以下类型不能用作代理:.Status:方法 set_StatusText 应该是“公共/受保护的虚拟”或“受保护的内部虚拟”..Status:方法 set_Location 应该是“公共/受保护”虚拟”或“受保护的内部虚拟”

因此,如果我按照建议更改类(唯一的更改是将“私有集”更改为“受保护集”),项目将不会构建,因为:

错误 2 CA2214:Microsoft.Usage:“Status.Status(string, StatusLocation)”包含一个调用链,该调用链导致调用该类定义的虚拟方法。查看以下调用堆栈以了解意外后果:Status..ctor(String, StatusLocation) Status.set_Location(StatusLocation):Void C:\\Status.cs 28

这就是为什么还需要更改构造函数中初始化这些虚拟属性之一的任何语句的原因。

NHibernate 代理 (ByteCode.Castle) 的先前实现似乎并不关心“私有集”,而这个实现。

而且,不可否认,第二个错误是因为 FxCop,我可以在类上放置一个属性来告诉 FxCop 不要抱怨这个,但这似乎只是让问题消失了,在构造函数中初始化虚拟属性是,据我了解,无论如何都是不好的做法。

所以我的问题仍然存在。我最初描述的更改是我想做的更改。如何自动化这些更改?

4

1 回答 1

1

我继续编写了一个 C# 实用程序来解析这样的类文件。“受保护的集合”的想法在某种程度上是有效的(感谢 Hazzik),但它仍然需要一个支持字段。下面的代码产生我上面描述的输出(除了使用“保护集”)。问候,-戴夫

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;

namespace ConsoleApplication3
{
// TODO:  write recursive algorithm to loop through directories
// TODO:  handle generic collections as Fluent NHibernate treats those differently

class Program
{
    public static string ConvertInitialCapitalToUnderscoreAndLowerCase(string input)
    {
        var firstChar = input.Substring(0, 1);
        var restOfStmt = input.Substring(1);
        var newFirst = "_" + firstChar.ToLower();
        var output = newFirst + restOfStmt;
        return output;
    }

    // this gets any tabs or spaces at the beginning of the line of code as a string,
    // so as to preserve the indentation (and/or add deeper levels of indentation)
    public static string GetCodeLineIndentation(string input)
    {
        var charArray = input.ToCharArray();

        var sbPrefix = new StringBuilder();

        foreach (var c in charArray)
        {
            // if it's a tab or a space, add it to the "prefix"
            if (c == 9 || c == ' ')
            {
                sbPrefix.Append(c);
            }
            else
            {
                // get out as soon as we hit the first ascii character (those with a value up to 127)
                break;
            }
        }

        return sbPrefix.ToString();
    }



    static void Main(string[] args)
    {

        const string path = @"C:\pathToFile\Status.cs";

        Console.WriteLine("Examining file:  " + path);

        if (!File.Exists(path))
        {
            Console.WriteLine("File does not exist:  " + path);
            throw new FileNotFoundException(path);
        }

        // Read the file.
        var arrayOfLines = File.ReadAllLines(path);

        // Convert to List<string>
        var inputFileAsListOfStrings = new List<string>(arrayOfLines);

        // See if there are any virtual properties.
        var virtualProps = inputFileAsListOfStrings.Where(s => s.Contains("public virtual")).ToList();

        // See if there are any "private set" strings.
        var privateSets = inputFileAsListOfStrings.Where(s => s.Contains("private set")).ToList();

        if (virtualProps.Count > 0)
        {
            Console.WriteLine("Found " + virtualProps.Count + " virtual properties in the class...");
        }

        if (privateSets.Count > 0)
        {
            Console.WriteLine("Found " + privateSets.Count + " private set statements in the class...");
        }

        // Get a list of names of the virtual properties
        // (the 4th "word", i.e. index = 3, in the string, will be the property name, 
        // e.g. "public virtual string SomePropertyName"
        var virtualPropNames = virtualProps.Select(vp => vp.Trim().Split(' ')).Select(words => words[3]).ToList();

        if (virtualPropNames.Count() != virtualProps.Count())
        {
            throw new Exception("Error:  the list of virtual property names does not equal the number of virtual property statements!");
        }

        // Find all instances of the virtual properties being initialized.

        // By examining the overall file for instances of the virtual property name followed by an equal sign,
        // we can identify those lines which are statements initializing the virtual property.
        var initializeStatements = (from vpName in virtualPropNames
                                    from stmt in inputFileAsListOfStrings
                                    let stmtNoSpaces = stmt.Trim().Replace(" ", "")
                                    where stmtNoSpaces.StartsWith(vpName + "=")
                                    select stmt).ToList();

        if (initializeStatements.Count() > 0)
        {
            Console.WriteLine("Found " + initializeStatements.Count + " initialize statements in the class...");
        }

        // now process the input based on the found strings and write the output
        var outputFileAsListOfStrings = new List<string>();

        foreach (var inputLineBeingProcessed in inputFileAsListOfStrings)
        {
            // is the input line one of the initialize statements identified previously?
            // if so, rewrite the line.

            // e.g.  
            // old line:  StatusText = statusText;  
            // becomes:  _statusText = statusText;

            var isInitStmt = false;
            foreach (var initStmt in initializeStatements)
            {
                if (inputLineBeingProcessed != initStmt) continue;

                // we've found our statement; it is an initialize statement;
                // now rewrite the format of the line as desired
                var prefix = GetCodeLineIndentation(inputLineBeingProcessed);

                var tabAndSpaceArray = new[] {' ', '\t'};

                var inputLineWithoutPrefix = inputLineBeingProcessed.TrimStart(tabAndSpaceArray);

                var outputLine = prefix + ConvertInitialCapitalToUnderscoreAndLowerCase(inputLineWithoutPrefix);

                // write the line (preceded by its prefix) to the output file
                outputFileAsListOfStrings.Add(outputLine);

                Console.WriteLine("Rewrote INPUT: " + initStmt + " to OUTPUT:  " + outputLine);
                isInitStmt = true;

                // we have now processed the input line; no need to loop through the initialize statements any further
                break;
            }

            // if we've already determined the current input line is an initialize statement, no need to proceed further;
            // go on to the next input line
            if (isInitStmt)
                continue;


            // is the input line one of the "public virtual SomeType SomePropertyName" statements identified previously?
            // if so, rewrite the single line as multiple lines of output.

            // the input will look like this:

            /*

            public virtual SomeType SomePropertyName { get; set; }

            */

            // first, we'll need a private variable which corresponds to the original statement in terms of name and type.

            // what we'll do is, write the private field AFTER the public property, so as not to interfere with the XML
            // comments above the "public virtual" statement.

            // the output will be SIX LINES, as follows:

            /*


            public virtual SomeType SomePropertyName
            {
                get { return _somePropertyName; }
                protected set { _someProperty = value; }
            }
            private SomeType _somePropertyName;



            */

            var isPublicVirtualStatement = false;

            foreach (var vp in virtualProps)
            {
                if (inputLineBeingProcessed != vp) continue;

                // the input line being processed is a "public virtual" statement;
                // convert it into the six line output format
                var thisOutputList = new List<string>();

                // first separate any indentation "prefix" that may exist (i.e. tabs and/or spaces),
                // from the actual string of text representing the line of code
                var prefix = GetCodeLineIndentation(inputLineBeingProcessed);

                var tabAndSpaceArray = new[] { ' ', '\t' };

                var inputLineWithoutPrefix = inputLineBeingProcessed.TrimStart(tabAndSpaceArray);

                var originalVpStmt = inputLineWithoutPrefix.Split(' ');

                // first output line (preceded by its original prefix)
                var firstOutputLine =   prefix + 
                                        originalVpStmt[0] + ' ' + 
                                        originalVpStmt[1] + ' ' + 
                                        originalVpStmt[2] + ' ' +
                                        originalVpStmt[3];
                thisOutputList.Add(firstOutputLine);

                // second output line (indented to the same level as the original prefix)
                thisOutputList.Add(prefix + "{");

                // get field name from property name
                var fieldName = ConvertInitialCapitalToUnderscoreAndLowerCase(originalVpStmt[3]);

                // third output line (indented with the prefix, plus one more tab)
                var thirdOutputLine = prefix + "\t" + "get { return " + fieldName + "; }";
                thisOutputList.Add(thirdOutputLine);

                // fourth output line (indented with the prefix, plus one more tab)
                var fourthOutputLine = prefix + "\t" + "protected set { " + fieldName + " = value; }";
                thisOutputList.Add(fourthOutputLine);

                // fifth output line (indented to the same level as the first curly bracket)
                thisOutputList.Add(prefix + "}");

                // sixth output line (the "index 2" value of the original statement will be the string representing the .Net type)
                // (indentation is the same as the "public virtual" statement above)

                var sixthOutputLine =   prefix + 
                                        "private" + ' ' +
                                        originalVpStmt[2] + ' ' +
                                        fieldName + ";";
                thisOutputList.Add(sixthOutputLine);

                // now write the six new lines to the master output list
                outputFileAsListOfStrings.AddRange(thisOutputList);

                isPublicVirtualStatement = true;
                Console.WriteLine("Rewrote INPUT: " + inputLineBeingProcessed + " to OUTPUT:  <multi-line block>");
                break;

            }

            // if we've already determined the current input line is a "public virtual" statement, no need to proceed further;
            // go on to the next input line
            if (isPublicVirtualStatement)
                continue;


            // if we've gotten this far, the input statement is neither an "initialize" statement, nor a "public virtual" statement;
            // So just write the output.  Don't bother logging this as most lines will not be ones we'll process.
            outputFileAsListOfStrings.Add(inputLineBeingProcessed);

        }

        // write the output file
        var newPath = path.Replace(".cs", "-NEW.cs");
        File.WriteAllLines(newPath, outputFileAsListOfStrings);

    }
}
于 2012-07-25T14:56:23.147 回答