Otomatik olarak kod oluşturmak için Visual Studio ve Kaynak Oluşturucu'yu kullanma

Sayfa güncel :
Sayfa oluşturma tarihi :

Çalışma ortamı

Visual Studio
  • Visual Studio 2022
.NET
  • .NET 8.0

Önkoşullar

Visual Studio
  • Biraz daha eski bir sürümle bile çalışır
.NET
  • Biraz daha eski bir sürümle bile çalışır

İlk başta

Kendi tanımlarınızla otomatik olarak kod oluşturmak için birkaç teknik vardır, ancak bu makalede size bir Kaynak Oluşturucuyu nasıl kullanacağınızı göstereceğim. Bir kaynak oluşturucunun en büyük avantajlarından biri, mevcut projenin kaynak kodunun yapısını analiz etmesi ve buna dayalı olarak yeni kod oluşturmasıdır. Örneğin, yeni bir sınıf oluşturduğunuzda, kodun sınıfla eşleşecek şekilde otomatik olarak eklenmesini sağlayabilirsiniz. Ne tür bir kod oluşturmak istediğinizi programlayabilir, böylece istediğiniz herhangi bir otomatik kod oluşturma biçimini oluşturabilirsiniz.

Kod esasen arka planda otomatik olarak oluşturulur ve perde arkasında projeye dahil edilir. Görünür bir şekilde dosya olarak çıktısı alınmadığı için, otomatik olarak oluşturulan kodun genel amaçlarla yeniden kullanılması amacıyla kullanılmaz (ancak şu an için kopyalanarak kaldırılabilir). Ancak kod, projenin yapısına göre otomatik olarak oluşturulduğundan, manuel giriş maliyetini düşürür ve buna bağlı olarak kod yazma hatalarını azaltır, bu da büyük bir avantajdır.

Bu yazıda, kodun otomatik olarak oluşturulup oluşturulmadığını nasıl kontrol edeceğimi açıklayacağım, bu yüzden kodu gerçekten derinlemesine analiz edecek ve gelişmiş çıktı gerçekleştirecek kadar ileri gitmeyeceğim. Lütfen kendiniz bir uygulama olarak arayın.

Kurulum

İlk olarak Visual Studio'yu yükleyin. Aşağıdaki İpuçlarında kısa bir açıklama özetlenmiştir.

Temel olarak, herhangi bir proje için kullanabilirsiniz, bu nedenle hangi iş yükünü ayarladığınız önemli değildir. Ancak bu sefer "bireysel bileşen" olarak, ". NET Derleyici Platformu SDK'sı. Bu, Kaynak Oluşturucu geliştirme sırasında hata ayıklama için kullanışlıdır. Visual Studio'yu zaten yüklediyseniz, Araçlar > Araçları ve Özellikleri Al altındaki Visual Studio menüsünden ekleyebilirsiniz.

Kaynak Oluşturucu Projesi Oluşturma ve Hazırlama

Kaynak Oluşturucu, ana uygulama projesinden ayrı bir projede oluşturulur. Önce bunları oluşturmanız veya daha sonra yenilerini oluşturmanız önemli değildir. Bu durumda Source Generator projesinden oluşturacağım.

Yeni Proje Oluştur ekranında Sınıf Kitaplığı'nı seçin.

Proje adı herhangi bir şey olabilir, ancak şimdilik CodeGenerator olarak bırakacağız.

Sınıf kitaplıkları için Kaynak Oluşturucu şu anda . NET Standard 2.0 olarak adlandırılır.

Projenizi oluşturduktan sonra NuGet ile Microsoft.CodeAnalysis.CSharp paketi alın. Davranış, sürüme bağlı olarak değişebilir, ancak eski sürümü kullanmaya devam etmenin bir anlamı yoktur, bu yüzden en son sürümü koyacağım.

Ardından proje dosyasını kod olarak açın.

Açtığınızda aşağıdakileri göreceksiniz.

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

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

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

</Project>

Aşağıdaki gibi ekleyin: İhtiyacınız olan başka bir şeyi eklemekten çekinmeyin.

<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>

Ardından, kodu otomatik olarak oluşturacak sınıfın kodunu yazın. Her şeyden önce, sadece bir çerçeve oluşturacağız, bu yüzden lütfen mevcut kodu baştan Class1.cs yazın veya yeni bir kod ekleyin.

Kod şöyle görünmelidir: Sınıf adı herhangi bir şey olabilir, ancak otomatik olarak ne tür bir kodun oluşturulduğunu gösteren bir ada sahip olmak daha iyidir. SampleGenerator Şimdilik olarak bırakın. Initialize Bu yöntemde, kod analizi ve kod oluşturma sürecini yazacaksınız.

using Microsoft.CodeAnalysis;

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

Ardından, otomatik olarak kod oluşturmak istediğiniz uygulama için bir proje oluşturun. Herhangi bir şey olabilir, ancak burada onu bir konsol uygulaması olarak kullanacağız. Kodun otomatik olarak oluşturulduğu proje çerçevesinin türü ve sürümü genel sayıya karşılık gelir.

Uygulama tarafı projesinden kaynak oluşturucu projesine bir başvuru ekleyin.

Bazı ayarlar özelliklerde ayarlanamaz, bu nedenle proje dosyasını kodda açın.

Sanırım şuna benziyor:

<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>

Başvurulan proje için ayarları aşağıdaki gibi ekleyin: XML'in sözdiziminde hata yapmadığınızdan emin olun.

<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>

Ardından, "Kod Oluşturucu tarafında" proje özelliklerini açın.

Hata Ayıklama Başlatma Özellikleri Kullanıcı Arabirimini Açmak için bağlantıya tıklayın.

Kullanmak istemediğiniz için orijinal profili silin.

Yeni bir profil ekleyin.

Roslyn Bileşeni'ni seçin.

Ayarları şimdiye kadar yaptıysanız, uygulama projesini seçebilmelisiniz, bu yüzden onu seçin.

Bu hazırlığın sonu.

Hata ayıklayıp ayıklayamayacağınızı kontrol edin

Kaynak oluşturucu kodunu açın ve Initialize yöntemin sonuna bir kesme noktası yerleştirin.

Kaynak oluşturucuda hata ayıklayalım.

İşlem kesme noktasında durursa, normal şekilde hata ayıkladığınızı onaylayabilirsiniz. Bu, kaynak oluşturucunuzun geliştirilmesini oldukça kolaylaştırmalıdır.

Şimdilik, sabit bir kod çıktısı alalım

İlk olarak, kolayca sabit bir kod çıktısı almaya çalışalım. Kolaydır çünkü kodu analiz etmenize bile gerek yoktur. Sabit bir kod olsa bile, bir dize olarak ele alınır, bu nedenle bir program oluşturarak sabit bir formda kod üretimini artırmak mümkündür.

Sabit bir kod çıktısı almak istiyorsanız, context.RegisterPostInitializationOutput bunu kullanarak yapabilirsiniz. Aşağıda kod çıktısının bir örneği verilmiştir.

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));
    });
  }
}

Kodun içeriği yorumlarda yazıldığı gibidir, bu yüzden ayrıntıları atlayacağım. Oluşturun ve hata olmadığından emin olun.

Derleme tamamlandığında, kod oluşturucu tarafından oluşturulan kodu görmek için uygulama projesinde "Çözümleyiciler"i genişletebilirsiniz.

Görmüyorsanız Visual Studio'yu yeniden başlatın ve denetleyin. Şu anda, yeniden başlatmadığınız sürece Visual Studio güncelleştirilmeyebilir gibi görünüyor. Program oluştururken kullanılan Intellisence ve sözdizimi vurgulama benzerdir. Ancak, kodun kendisi derleme sırasında yansıtıldığından, programın uygulama tarafına yansıdığı görülüyor.

Kod oluşturulduktan sonra uygulamanızda kullanmayı deneyin. İyi çalışmalı.

Kodu analiz etme ve oluşturma

Kodu normal şekilde işler ve çıktısını alırsanız, diğer otomatik kod üretiminden çok farklı değildir ve kaynak oluşturucunun avantajlarından yararlanamazsınız. Şimdi uygulama projesinin kodunu analiz edelim ve kodu buna göre oluşturalım.

Ancak, kodun analizi oldukça derin, bu yüzden burada her şeyi açıklayamam. Şimdilik, kodu analiz edebileceğiniz ve çıktısını alabileceğiniz noktaya kadar açıklayacağım. Daha derine inmek istiyorsanız, lütfen kendi araştırmanızı yapın.

Şimdilik, örnek olarak, "oluşturulan tüm sınıflara Reset bir yöntem ekleyin ve tüm özelliklerin değerini default " olarak sıfırlayın kodunu otomatik olarak oluşturmak istiyorum. Son yıllarda immutable instance'ların işlenmesi tercih ediliyor gibi görünüyor ancak oyun programlarında her frame'de yeni bir instance oluşturmak mümkün değil, bu yüzden değeri sıfırlama işlemi için bir kullanım olduğunu düşünüyorum. default Bunun dışında bir şey ayarlamak isteyebilirsiniz, ancak bunu yaparsanız, uzun bir ilk örnek olacaktır, bu yüzden lütfen kendiniz uygulayın.

Kod Oluşturucu Tarafı

Daha önce oluşturduğunuz oluşturucuyu korumak amacıyla yeni bir sınıf oluşturun. Otomatik olarak oluşturulacak içerik değişirse, başka bir sınıfta yeni bir tane oluşturmak daha iyidir.

Roslyn derleyicisi kodun yapılandırılmasıyla ilgilenir, bu yüzden burada yapacağımız şey yapılandırılmış verileri ayrıştırmak ve kodu yazmaktır.

İlk olarak, tüm kodu göndereceğim. Mümkün olduğunca az kod kullanmaya çalıştım. Bu sefer işe yarıyor, ancak sadece bir Bay/Bayan olarak hareket eden bir seviyede, bu yüzden gerçekten geliştirmeye devam ettiğinizde, epeyce eksik parça olacak. Lütfen orayı gerektiği gibi geliştirin.

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);
    });
  }
}

Yorumlarda yazdım ama bazı yerlerde açıklayacağım.

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

context.SyntaxProvider.CreateSyntaxProvider uygulama projesindeki kodu analiz etmek ve mümkün olduğunca yapılandırmak. Her şey düğümlere bölünmüştür ve hangisini işlemek istediğimizi predicate belirlemek için kullanırız. Ayrıca, gerekirse transform , işlenen nesneyi ile dönüştürün ve çıktı tarafına iletin.

predicate Bu durumda, aşağıdakileri yapıyoruz.

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

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

Her şeyden önce, herhangi bir işlemde olduğu gibi, otomatik olarak kod oluşturma işlemi her zaman erken iptal edilebilir. Bu nedenle, istediğiniz zaman kesintiye uğrayabilmeniz için çağırmak için ThrowIfCancellationRequested iptal belirtecini kullanın.

predicate Artık her düğüm çağrıldığına göre, hangisinin işlemek istediğimizi belirlemek istiyoruz. Çok sayıda olduğu için, onları burada bir dereceye kadar daraltmak daha iyidir.

Bu sefer ClassDeclarationSyntax class'a processing ekleyeceğimiz için olup olmadığını belirleyip sadece class'ın mı işleneceğini belirleyeceğiz. partial class Ayrıca, kod ile ekli olduğundan, partial class bir yargı olarak konur.

// 対象のノードをコード出力に必要な形に変換します
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 Analizi gerekli forma dönüştürmenize ve kod çıktısına geçirmenize olanak tanır. Bu sefer çok fazla dönüşüm yapmıyoruz ama değeri olduğu return gibi çıktı tarafına geçiriyoruz. Yol boyunca "beyan edilen semboller" alıyoruz, ancakSemanticModel ve Syntac kullanarak birçok ek bilgi alabiliriz, bu yüzden gerekirse alabileceğimizi düşünüyorum. return Demetler oluşturulur ve döndürülür, ancak çıkış tarafına geçirilecek verilerin yapılandırması herhangi bir şey olabilir. Birden çok veri iletmek istiyorsanız, bunun gibi bir tanımlama grubu oluşturabilir veya kendi sınıfınızı tanımlayabilir ve iletebilirsiniz.

// 解析し変換した情報をもとにコードを出力します
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 Şimdi, tarafından işlenecek verilere dayalı olarak kod oluşturacağız ve çıktısını alacağız. context.SyntaxProvider.CreateSyntaxProvider return'nin değeri, 'nin Action ikinci bağımsız değişkeni olarak alınabilir, bu nedenle kod bu değere göre oluşturulur.

Bu durumda, sınıfın sözdiziminden özelliklerin sözdizimi listesini almak ve özelliği from each name olarak ayarlamak default için kod oluşturuyoruz.

Bundan sonra, sınıf adını temel alan Reset bir yöntem oluşturun, daha önce oluşturulan özellik listesinin kodunu gömün ve tüm özelliklerin değerini default Yöntem Reset tamamlandı olarak ayarlayın.

Kod, contextSource.AddSource yöntemdeki her sınıf için ayrı ayrı çıkarılır. Bu arada dosya ismine "g" koymamın sebebi build hatası olduğunda manuel olarak oluşturulmuş kod mu yoksa otomatik olarak oluşturulan kod hatası mı olduğunu belirlemeyi kolaylaştırmak.

Gerçekten yaparsanız, "Alanı sıfırlamak istiyorum", "Varsayılandan başka bir şekilde başlatmak istiyorum", "Yalnızca otomatik özellikleri sıfırlamak istiyorum" gibi istekler alacaksınız. Onları koyarsanız, kablo uzun olacaktır, bu yüzden kendiniz yapmaya çalışın.

Uygulama projesi tarafı

Bu sefer sınıfın metodunu genişleten bir kod oluşturdum, bu yüzden uygun bir sınıf oluşturacağım.

İçerikler aşağıdaki gibidir, ancak özellikler varsa, geri kalanını uygun şekilde kullanabilirsiniz.

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}}}";
  }
}

Kodu oluşturduğunuzda yöntem genişletilir Reset , ancak Visual Studio'ya yansıtılmayabilir, bu nedenle Visual Studio'yu o sırada yeniden başlatın. Kodun otomatik olarak analizöre genişletildiğini görebildiğinizi düşünüyorum.

Girinti gariptir, ancak temelde otomatik olarak oluşturulan koda dokunmadığınız için bunu görmezden gelebilirsiniz.

Program.cs Yöntemi çağırıp çağıramayacağınızı görmek Reset için kod yazmayı deneyin. Yürütmenin sonuçlarının beklendiği gibi yansıtıldığını düşünüyorum.

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();