שימוש ב- Visual Studio וב- Source Generator כדי ליצור קוד באופן אוטומטי

עודכן דף :
תאריך יצירת דף :

סביבת הפעלה

Visual Studio
  • Visual Studio 2022
.רשת
  • .NET 8.0

דרישות מוקדמות

Visual Studio
  • זה עובד גם עם גרסה קצת יותר מבוגרת
.רשת
  • זה עובד גם עם גרסה קצת יותר מבוגרת

בהתחלה

ישנן מספר טכניקות ליצירת קוד באופן אוטומטי עם הגדרות משלך, אך במאמר זה אראה לך כיצד להשתמש במחולל מקור. אחד היתרונות הגדולים ביותר של מחולל מקור הוא שהוא מנתח את מבנה קוד המקור של הפרויקט הנוכחי ומייצר קוד חדש המבוסס עליו. לדוגמה, בעת יצירת מחלקה חדשה, באפשרותך ליצור אותה כך שהקוד יתווסף באופן אוטומטי כך שיתאים למחלקה. אתה יכול לתכנת איזה סוג קוד אתה רוצה ליצור, כך שתוכל ליצור כל צורה של יצירת קוד אוטומטית שתרצה.

הקוד למעשה נוצר באופן אוטומטי ברקע ומשולב בפרויקט מאחורי הקלעים. מכיוון שהוא אינו מופק כקובץ באופן גלוי, הוא אינו משמש לצורך שימוש חוזר בקוד שנוצר אוטומטית למטרות כלליות (אם כי ניתן להסיר אותו על ידי העתקתו לעת עתה). עם זאת, מכיוון שהקוד נוצר אוטומטית בהתאם למבנה הפרויקט, הוא מוזיל את עלות הקלט הידני ומפחית טעויות כתיבת קוד בהתאם, וזה יתרון עצום.

במאמר זה אסביר כיצד לבדוק שהקוד נוצר באופן אוטומטי, כך שלא ארחיק לכת עד כדי ניתוח הקוד לעומק וביצוע פלט מתקדם. אנא חפש זאת בעצמך כיישום.

ההתקנה

תחילה, התקן את Visual Studio. הסבר קצר מסוכם בטיפים הבאים.

בעיקרון, באפשרותך להשתמש בו עבור כל פרוייקט, כך שלא משנה איזה עומס עבודה הגדרת. עם זאת, הפעם, כ"מרכיב בודד", ". NET Compiler Platform SDK. אפשרות זו שימושית לאיתור באגים במהלך פיתוח מחולל מקורות. אם Visual Studio כבר מותקן במחשב שלך, באפשרותך להוסיף אותו מתפריט Visual Studio תחת כלים > קבל כלים ותכונות.

יצירה והכנה של פרויקט מחולל מקור

מחולל המקור נוצר בפרויקט נפרד מפרויקט היישום הראשי. אין זה משנה אם אתה יוצר אותם תחילה או יוצר קבצים נוספים מאוחר יותר. במקרה זה, אני אצור אותו מפרויקט מחולל המקור.

במסך יצירת פרוייקט חדש, בחר ספריית כיתות.

שם הפרויקט יכול להיות כל דבר, אבל לעת עתה CodeGenerator , נשאיר אותו כ- .

עבור ספריות מחלקות, Source Generator תומך כעת ב- . NET Standard 2.0.

לאחר שיצרת את הפרויקט שלך, קבל את החבילה עם Microsoft.CodeAnalysis.CSharp NuGet. ההתנהגות עשויה להשתנות בהתאם לגרסה, אבל אין טעם להמשיך להשתמש בגרסה הישנה, אז אני אשים את הגרסה האחרונה.

לאחר מכן פתח את קובץ הפרויקט כקוד.

כאשר תפתח אותו, תראה את הדברים הבאים.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
  </ItemGroup>

</Project>

הוסף אותו באופן הבא: אל תהסס להוסיף כל דבר אחר שאתה צריך.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>

    <!-- Roslyn によるコード解析を行う -->
    <IsRoslynComponent>true</IsRoslynComponent>
    <!-- 入れないと警告が表示されるので入れておく -->
    <EnforceExtendedAnalyzerRules>true</EnforceExtendedAnalyzerRules>
    <!-- 最新の C# の記述を使いたいので入れておく -->
    <LangVersion>Latest</LangVersion>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
  </ItemGroup>

</Project>

לאחר מכן, כתוב את הקוד עבור הכיתה שתיצור את הקוד באופן אוטומטי. קודם כל, אנחנו רק ליצור מסגרת, אז בבקשה לשכתב את הקוד הקיים מההתחלה Class1.cs או להוסיף קוד חדש.

הקוד אמור להיראות כך: שם הכיתה יכול להיות כל דבר, אבל עדיף שיהיה שם שמראה איזה סוג של קוד נוצר באופן אוטומטי. SampleGenerator לעת עתה, השאר את זה כמו . Initialize בשיטה זו תכתוב את תהליך ניתוח הקוד ויצירת הקוד.

using Microsoft.CodeAnalysis;

namespace CodeGenerator
{
  [Generator(LanguageNames.CSharp)]
  public partial class SampleGenerator : IIncrementalGenerator
  {
    public void Initialize(IncrementalGeneratorInitializationContext context)
    {
    }
  }
}

לאחר מכן, צור פרויקט עבור היישום שעבורו ברצונך ליצור קוד באופן אוטומטי. זה יכול להיות כל דבר, אבל כאן פשוט נשתמש בו כיישום קונסולה. הסוג והגירסה של מסגרת הפרויקט שאליו נוצר הקוד באופן אוטומטי תואמים את המספר הכללי.

הוסף הפניה לפרוייקט מחולל המקור מהפרוייקט בצד היישום.

לא ניתן להגדיר הגדרות מסוימות במאפיינים, לכן פתח את קובץ הפרויקט בקוד.

אני חושב שזה נראה כך:

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\CodeGenerator\CodeGenerator.csproj" />
  </ItemGroup>

</Project>

הוסף את ההגדרות עבור הפרוייקט שבוצעה אליו הפניה באופן הבא: ודא שאינך טועה בתחביר של ה- XML.

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <ProjectReference Include="..\CodeGenerator\CodeGenerator.csproj">
      <OutputItemType>Analyzer</OutputItemType>
      <ReferenceOutputAssembly>False</ReferenceOutputAssembly>
    </ProjectReference>
  </ItemGroup>

</Project>

לאחר מכן, פתח את מאפייני הפרויקט בצד "מחולל קוד".

לחץ על הקישור כדי לפתוח את ממשק המשתמש של מאפייני הפעלת איתור באגים.

מחק את הפרופיל המקורי מכיוון שאינך מעוניין להשתמש בו.

הוסף פרופיל חדש.

בחר רכיב רוזלין.

אם ביצעת את ההגדרות עד כה, אתה אמור להיות מסוגל לבחור את פרויקט היישום, אז בחר אותו.

זה סוף ההכנה.

בדוק אם אתה יכול לאתר באגים

פתח את קוד מחולל המקור והצב Initialize נקודת עצירה בסוף השיטה.

בואו ניפוי באגים במחולל המקור.

אם התהליך נעצר בנקודת ההפסקה, באפשרותך לאשר שאתה מאתר באגים כרגיל. זה אמור להפוך את הפיתוח של מחולל המקור שלך לקל למדי.

לעת עתה, בואו נוציא קוד קבוע

ראשית, בואו ננסה להפיק קוד קבוע בקלות. זה קל כי אתה אפילו לא צריך לנתח את הקוד. גם אם מדובר בקוד קבוע, הוא מטופל כמחרוזת, כך שניתן להגדיל את ייצור הקוד בצורה קבועה על ידי בניית תוכנית.

אם ברצונך להפיק קוד קבוע, תוכל context.RegisterPostInitializationOutput לעשות זאת באמצעות . להלן דוגמה לפלט הקוד.

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
using System.Text;

namespace CodeGenerator;

[Generator(LanguageNames.CSharp)]
public partial class SampleGenerator : IIncrementalGenerator
{
  public void Initialize(IncrementalGeneratorInitializationContext context)
  {
    // コードを解析せずそのままコードを出力するならこれを使用します
    context.RegisterPostInitializationOutput(static postInitializationContext =>
    {
      // コード生成処理が中断されることもあるので CancellationToken 処理は入れたほうがいいです
      System.Threading.CancellationToken token = postInitializationContext.CancellationToken;
      token.ThrowIfCancellationRequested();

      // 出力するコードを作ります
      var source = """
internal static class SampleClass
{
  public static void Hello() => Console.WriteLine("Hello Source Generator!!");
}
""";
      
      // コードファイルに出力します (実際に目に見えるどこかのフォルダに出力されるわけではありません)
      postInitializationContext.AddSource("SampleGeneratedFile.cs", SourceText.From(source, Encoding.UTF8));
    });
  }
}

תוכן הקוד הוא כפי שנכתב בהערות, ולכן אשמיט את הפרטים. בנה אותו וודא שאין שגיאות.

לאחר השלמת הבנייה, באפשרותך להרחיב את "אנלייזרים" בפרויקט היישום כדי לראות את הקוד שנוצר על ידי מחולל הקוד.

אם אינך רואה אותו, הפעל מחדש את Visual Studio ובדוק אותו. נראה כי בשלב זה, Visual Studio לא יעודכן אלא אם תפעיל אותו מחדש. הדגשת האינטליזציה והתחביר המשמשים ליצירת תוכניות דומים. עם זאת, מכיוון שהקוד עצמו משתקף בזמן הבנייה, נראה כי התוכנית משתקפת בצד היישום.

לאחר יצירת הקוד, נסה להשתמש בו ביישום שלך. זה אמור לעבוד מצוין.

ניתוח ויצירת קוד

אם אתה רק מעבד את הקוד כרגיל ומפיק אותו, זה לא שונה בהרבה מיצירת קוד אוטומטית אחרת, ואתה לא יכול לנצל את היתרונות של מחולל המקור. אז עכשיו בואו לנתח את הקוד של פרויקט היישום וליצור את הקוד בהתאם.

עם זאת, הניתוח של הקוד הוא עמוק למדי, אז אני לא יכול להסביר הכל כאן. לעת עתה, אסביר עד לנקודה בה תוכלו לנתח ולהפיק את הקוד. אם אתה רוצה להעמיק, בבקשה לעשות מחקר משלך.

לעת עתה, כדוגמה, ברצוני ליצור באופן אוטומטי את הקוד "הוסף שיטה לכל Reset המחלקות שנוצרו ולאפס את הערך default של כל המאפיינים ל- ". בשנים האחרונות נראה כי הטיפול במופעים בלתי ניתנים לשינוי הועדף, אך בתוכנות משחק לא ניתן ליצור מופע חדש בכל פריים, ולכן אני חושב שיש שימוש בתהליך איפוס הערך. default ייתכן שתרצה להגדיר משהו אחר מזה, אבל אם תעשה זאת, זו תהיה דוגמה ראשונה ארוכה, אז אנא יישם אותה בעצמך.

צד מחולל קוד

צור מחלקה חדשה לצורך שמירה על הגנרטור שיצרת בעבר. אם התוכן שייווצר באופן אוטומטי משתנה, עדיף ליצור אחד חדש בכיתה אחרת.

המהדר של רוזלין דואג למבנה הקוד, אז מה שאנחנו הולכים לעשות כאן הוא לנתח את הנתונים המובנים ולכתוב את הקוד.

ראשית, אפרסם את כל הקוד. ניסיתי שיהיה לי כמה שפחות קוד. הפעם זה עובד, אבל זה רק ברמה שנעה כמר / גברת, כך שכאשר אתה באמת ממשיך עם הפיתוח, יהיו לא מעט חלקים חסרים. אנא שפר שם לפי הצורך.

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using System.Linq;
using System.Text;
using System.Threading;

namespace CodeGenerator;

[Generator(LanguageNames.CSharp)]
public partial class Sample2Generator : IIncrementalGenerator
{
  public void Initialize(IncrementalGeneratorInitializationContext context)
  {
    // 構文を解析し処理対象のノードをコード出力用に変換します
    var syntaxProvider = context.SyntaxProvider.CreateSyntaxProvider(
      // すべてのノードが処理対象となるため、predicate で処理対象とするノードを限定します
      predicate: static (node, cancelToken) =>
      {
        // 処理が中断される場面は多々あるのでいつでもキャンセルできるようにしておく
        cancelToken.ThrowIfCancellationRequested();

        // クラスのノードのみを対象とする (record とかつけると別な種類のノードになるので注意)
        // また partial class であること
        return node is ClassDeclarationSyntax nodeClass && nodeClass.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword));
      },
      // 対象のノードをコード出力に必要な形に変換します
      transform: static (contextSyntax, cancelToken) =>
      {
        // いつでもキャンセルできるようにしておく
        cancelToken.ThrowIfCancellationRequested();

        // 今回は ClassDeclarationSyntax 確定なのでそのままキャストします
        ClassDeclarationSyntax classDecl = (ClassDeclarationSyntax)contextSyntax.Node;

        // データと構文から宣言されたシンボルを取得します
        // 今回利用場面がほとんどないですが取得しておくといろんな情報を取得できるので便利です
        var symbol = Microsoft.CodeAnalysis.CSharp.CSharpExtensions.GetDeclaredSymbol(contextSyntax.SemanticModel, classDecl, cancelToken)!;

        // 出力に必要な値を返します。大抵の型を渡すことができます
        return (symbol, classDecl);
      }
    );

    // 解析し変換した情報をもとにコードを出力します
    context.RegisterSourceOutput(syntaxProvider, static (contextSource, input) =>
    {
      // いつでもキャンセルできるようにしておく
      CancellationToken cancelToken = contextSource.CancellationToken;
      cancelToken.ThrowIfCancellationRequested();

      // 解析後に渡された値を受け取ります
      var (symbol, classDecl) = input;

      StringBuilder sbInitValues = new();

      // プロパティを列挙して値を初期化するコードを生成
      foreach (var syntax in classDecl.Members.OfType<PropertyDeclarationSyntax>())
      {
        sbInitValues.AppendLine($"{syntax.Identifier.ValueText} = default;");
      }

      // 出力するコードを作成
      var source = @$"
        {string.Join(" ", classDecl.Modifiers.Select(x => x.ToString()))} class {symbol.Name}
        {{
          public void Reset()
          {{
            {sbInitValues}
          }}
        }}";

      // ファイル名はクラス名に合わせておきます。作成したコードを出力します
      contextSource.AddSource($"{symbol.Name}.g.cs", source);
    });
  }
}

כתבתי על זה בתגובות, אבל אני אסביר את זה בכמה מקומות.

// 構文を解析し処理対象のノードをコード出力用に変換します
var syntaxProvider = context.SyntaxProvider.CreateSyntaxProvider(
  // すべてのノードが処理対象となるため、predicate で処理対象とするノードを限定します
  predicate: ...,
  // 対象のノードをコード出力に必要な形に変換します
  transform: ...
);

context.SyntaxProvider.CreateSyntaxProvider לנתח את הקוד בפרויקט היישום ולבנות אותו ככל האפשר. הכל מחולק לצמתים, ואנחנו משתמשים כדי לקבוע איזה מהם אנחנו predicate רוצים לעבד. כמו כן, במידת הצורך transform , להמיר את האובייקט המעובד עם ולהעביר אותו לצד הפלט.

predicate במקרה זה, אנו מבצעים את הפעולות הבאות.

// すべてのノードが処理対象となるため、predicate で処理対象とするノードを限定します
predicate: static (node, cancelToken) =>
{
  // 処理が中断される場面は多々あるのでいつでもキャンセルできるようにしておく
  cancelToken.ThrowIfCancellationRequested();

  // クラスのノードのみを対象とする (record とかつけると別な種類のノードになるので注意)
  // また partial class であること
  return node is ClassDeclarationSyntax nodeClass && nodeClass.Modifiers.Any(m => m.IsKind(SyntaxKind.PartialKeyword));
},

קודם כל, כמו בכל תהליך, תהליך של יצירת קוד אוטומטית תמיד יכול להתבטל בטרם עת. אז השתמש באסימון הביטול כדי ThrowIfCancellationRequested להתקשר כך שתוכל להפריע בכל עת.

predicate כעת, כשכל צומת נקרא, אנו רוצים לקבוע איזה צומת הוא זה שברצוננו לעבד. מכיוון שיש מספר עצום מהם, עדיף לצמצם אותם כאן במידה מסוימת.

מכיוון שנוסיף עיבוד לכיתה הפעם, נקבע ClassDeclarationSyntax אם זה כן ונקבע אם רק הכיתה תעובד. partial class כמו כן, מכיוון שהקוד מצורף עם , הוא partial class מוכנס כפסק דין.

// 対象のノードをコード出力に必要な形に変換します
transform: static (contextSyntax, cancelToken) =>
{
  // いつでもキャンセルできるようにしておく
  cancelToken.ThrowIfCancellationRequested();

  // 今回は ClassDeclarationSyntax 確定なのでそのままキャストします
  ClassDeclarationSyntax classDecl = (ClassDeclarationSyntax)contextSyntax.Node;

  // データと構文から宣言されたシンボルを取得します
  // 今回利用場面がほとんどないですが取得しておくといろんな情報を取得できるので便利です
  var symbol = Microsoft.CodeAnalysis.CSharp.CSharpExtensions.GetDeclaredSymbol(contextSyntax.SemanticModel, classDecl, cancelToken)!;

  // 出力に必要な値を返します。大抵の型を渡すことができます
   return (symbol, classDecl);
}

transform מאפשר לך להמיר את הניתוח לטופס הנדרש ולהעביר אותו לפלט הקוד. הפעם, אנחנו לא עושים הרבה המרה, אבל להעביר את הערך לצד הפלט כפי שהוא return . אנחנו מקבלים "סמלים מוצהרים" לאורך הדרך, אבלSemanticModel אנחנו יכולים לקבל הרבה מידע נוסף באמצעות ו Syntac , אז אני חושב שאנחנו יכולים לקבל את זה במידת הצורך. return tuples נוצרים ומוחזרים, אך תצורת הנתונים שיועברו לצד הפלט יכולה להיות כל דבר. אם ברצונך להעביר נתונים מרובים, באפשרותך ליצור tuple כזה, או להגדיר מחלקה משלך ולהעביר אותה פנימה.

// 解析し変換した情報をもとにコードを出力します
context.RegisterSourceOutput(syntaxProvider, static (contextSource, input) =>
{
  // いつでもキャンセルできるようにしておく
  CancellationToken cancelToken = contextSource.CancellationToken;
  cancelToken.ThrowIfCancellationRequested();

  // 解析後に渡された値を受け取ります
  var (symbol, classDecl) = input;

  StringBuilder sbInitValues = new();

  // プロパティを列挙して値を初期化するコードを生成
  foreach (var syntax in classDecl.Members.OfType<PropertyDeclarationSyntax>())
  {
    sbInitValues.AppendLine($"{syntax.Identifier.ValueText} = default;");
  }

  // 出力するコードを作成
  var source = @$"
    {string.Join(" ", classDecl.Modifiers.Select(x => x.ToString()))} class {symbol.Name}
    {{
      public void Reset()
      {{
        {sbInitValues}
      }}
    }}";

  // ファイル名はクラス名に合わせておきます。作成したコードを出力します
  contextSource.AddSource($"{symbol.Name}.g.cs", source);
});

context.RegisterSourceOutputcontext.SyntaxProvider.CreateSyntaxProvider כעת, אנו ניצור ונפיק קוד בהתבסס על הנתונים שיעובדו על ידי . context.SyntaxProvider.CreateSyntaxProvider return ניתן לקבל את הערך של כארגומנט השני של Action , כך שהקוד נוצר בהתבסס על ערך זה.

במקרה זה, אנו יוצרים קוד כדי לקבל את רשימת התחביר של מאפיינים מהתחביר של המחלקה ולהגדיר את default המאפיין לכל שם.

לאחר מכן, צור שיטה המבוססת על Reset שם המחלקה, הטבע את הקוד של רשימת המאפיינים שנוצרה קודם לכן והגדר את הערך default של כל המאפיינים בחזרה Reset לפעולת השירות הושלמה.

הקוד מופק contextSource.AddSource בנפרד עבור כל מחלקה בשיטה. אגב, הסיבה שאני שם "g" בשם הקובץ היא כדי להקל על קביעת אם זה קוד שנוצר באופן ידני או שגיאת קוד שנוצר באופן אוטומטי כאשר יש שגיאת בנייה.

אם אתה באמת עושה את זה, תקבל בקשות כגון "אני רוצה לאפס את השדה", "אני רוצה לאתחל אותו מלבד ברירת מחדל", "אני רוצה לאפס רק את המאפיינים האוטומטיים". אם אתה שם אותם, הכבל יהיה ארוך, אז לנסות לעשות את זה בעצמך.

צד פרוייקט יישום

הפעם, יצרתי קוד שמרחיב את השיטה של הכיתה, אז אני אצור כיתה כראוי.

התוכן הוא כדלקמן, אבל אם יש מאפיינים, אתה יכול להשתמש בשאר לפי הצורך.

Player.cs

public partial class Player
{
  public string Name { get; set; } = "";


  public float PositionX { get; set; }
  public float PositionY { get; set; }

  public int Level { get; set; } = 1;

  public override string ToString()
  {
    return $"{nameof(Player)} {{{nameof(Name)} = {Name}, {nameof(PositionX)} = {PositionX}, {nameof(PositionY)} = {PositionY}, {nameof(Level)} = {Level}}}";
  }
}

Item.cs

public partial class Item
{
  public string Name { get; set; } = "";

  public float PositionX { get; set; }
  public float PositionY { get; set; }

  public int Type { get; set; }

  public override string ToString()
  {
    return $"{nameof(Item)} {{{nameof(Name)} = {Name}, {nameof(PositionX)} = {PositionX}, {nameof(PositionY)} = {PositionY}, {nameof(Type)} = {Type}}}";
  }
}

השיטה מורחבת בעת Reset יצירת הקוד, אך ייתכן שהיא לא תבוא לידי ביטוי ב- Visual Studio, לכן הפעל מחדש את Visual Studio בשלב זה. אני חושב שאתה יכול לראות שהקוד מורחב אוטומטית למנתח.

הכניסה מוזרה, אבל אתה יכול להתעלם ממנה כי אתה בעצם לא נוגע בקוד שנוצר אוטומטית.

Program.cs נסה לכתוב קוד כדי לראות Reset אם אתה יכול להתקשר לשיטה. אני חושב שתוצאות הביצוע באות לידי ביטוי כצפוי.

Program.cs

Console.WriteLine("Hello, World!");

SampleClass.Hello();
Console.WriteLine();

Player player = new()
{
  Name = "Mike",
  PositionX = 10,
  PositionY = 5.5f,
  Level = 3,
};
Console.WriteLine(player);
player.Reset();
Console.WriteLine(player);
Console.WriteLine();

Item item = new()
{
  Name = "Banana",
  PositionX = 50,
  PositionY = 53.5f,
  Type = 12,
};
Console.WriteLine(item);
item.Reset();
Console.WriteLine(item);
Console.WriteLine();