.NET CORE 1.Microsoft Azure 微軟擁抱雲計算 2..net core 是為雲所生的技術 3.Net Framework缺點: 系統級別的安裝,互相影響 無法獨立部署 SAP.NET和IIS深度耦合 非雲原生 4.NET Framework歷史包袱 基於拖控制項之上的MVC A ...
1.Microsoft Azure 微軟擁抱雲計算
2..net core 是為雲所生的技術
3.Net Framework缺點:
-
-
無法獨立部署
-
SAP.NET和IIS深度耦合
-
非雲原生
4.NET Framework歷史包袱
-
基於拖控制項之上的MVC
-
ASP.NET 底層不支持很好的單元測試
5.NET Core的有點
-
支持獨立部署不相互影響
-
徹底模塊化
-
運行效率高
-
跨平臺
-
符合現代開發理念,依賴註入,單元測試等
.NET Core .NET Framework 兩者根據標準實現 .NET Standard 規定的類 規定的方法 只有定義沒有實現 反編譯軟體ILSpy
驗證:.NET Standard只是標準,不是實現1)建.NET Standard類庫項目,確認版本是 2.0,建一個類,方法中列印 typeof (FileStream).Assembly.Location。2分別建NET Framework和.NET Core的控制台項目,添加對類庫項目引用,並且調用。3用反編譯工具ILSpy儘管開源)分別反編譯VS中FileStream、.NET Framework和. NETCore運行中的。BeginRead方法實現以及定義有 不同。
.NET5開發工具:1).NET CLI:命令行 2)yisual Studio: Windows-Only(推薦)3) Visual Studio for Mac 4)Jetbrains Rider:收費 5)vS Code (Visual Studio, Code):跨平臺
.NET SDK、運行時、文檔----https:// dotnet.mi crosoft.com/ 可能VS自帶,但是在伺服器上需要單獨安裝
dotnet —-version查看版本 dotnet new console當前文件夾下創建控制台項目 dotnet run構建並運行 詳細見官方文檔“.NET CLI”部分。
程式的發佈。 1、部署模式:依賴框架;獨立(推薦)﹔ 2、目標運行時。 3、生成單個文件。 4、ReadyToRun: AOT (ahead of-time)、JIT。缺點看文檔。 5、裁剪未使用的程式集。缺點看文檔。 自學就要養成把相關文檔“翻一翻”的意識。
WSL:Windows subsystem for linux SandBox
NugGet
註意:【預設項目】為目標項目。 1)安裝:Install-Package XXX.yersion指定版本。 可以看到把依賴組件都下載了。 版本檢測:嘗試把項目改為.NET core 1.0,然後再安裝Zack. EFCore.Batch試試。 2)卸載:Uninstall-Package XXX 3)更新到最新版:Update-Package XXX
1、NuGet你也可以頁獻,就三步。 2、和.NET Framework不同,.NET core絕大部分官方程式集也要到NuGet下載。模塊化! 3、少部分是收費的,如搜索“word file” 4、參差不齊,如何分辨質量。 5、內部部署NuGet服務mTo
非同步編程1
不等 非同步編程不能加快單個請求的響應速度,只是能處理更多的請求
C#關鍵字:async await 不能與多線程
“非同步方法”:async關鍵字修飾的方法 1))非同步方法的返回值一般是Task<T>,T是真正的返回值類型,Task<int>。慣例:非同步方法名字以Async結尾。 2)即使方法沒有返回值,也最好把返回值聲明為非泛型的Task。 3)調用非同步方法時,一般在方法前加上await,這樣拿到的返回值就是泛型指定的T類型;4)非同步方法的“傳染性”:一個方法中如果有await調用,則這個方法也必須修飾為async
static async Task Main(string[] args)
{
string fileName = "d:/1.txt" ;File. Delete(fileName) ;
File.WriteA11TextAsync(fileName,"hello async");string s= await File.ReadAllTextAsync(fileName) ; Console.WriteLine(s):
}
如果同樣的功能,既有同步方法,又有非同步方法,那麼首先使用非同步方法。.NE5中,很多框架的方法也都支持非同步:Main、WinForm事件處理函數。 對於不支持的非同步方法怎麼辦? Wait()(無返回值);Result(有返回值)。風險:死鎖。盡不用
非同步委托
//線程池 在子線程中執行的方法放到線程池
ThreadPool.QueueUserWorkItem(async(obj) =>
{
while (true)
{
await File.WriteAllTextAsync(@"D:\text\1.txt","aaaaaaaaaaa");
Console.WriteLine("------------------");
}
});
Console.Read();
·用ILSpy反編譯dll(.exe只是 windows下的啟動器)成C#4.0版本,就能看到容易理解的底層lL代碼。await、async是“語法糖”,最終編譯成“狀態機調用”.
總結: async的方法會被C#編譯器編譯成一個類,會主要根據await調用進行切分為多個狀態,對async方法的調用會被拆分為對MoveNext的調用。 用await看似是“等待”,經過編譯後,其實沒有“wait”。
//使用非同步編程獲取百度網址頁面的代碼並且寫道文件中 然後讀出來寫入到控制器
using (HttpClient httpContent = new HttpClient())
{
string html = await httpContent.GetStringAsync("https://www.baidu.com");
Console.WriteLine(html);
}
string txt = "hellow zyb";
string filename = @"D:\text\1.txt";
await File.WriteAllTextAsync(filename, txt);
Console.WriteLine("寫入成功");
string s = await File.ReadAllTextAsync(filename);
Console.WriteLine("文件內容"+s);
await調用的等待期間,.NET會把當前的線程返回給線程池,等非同步方法調用執行完畢後,框架會從線程池再取出來一個線程執行後續的代碼。
Thread.CurrentThread.ManagedThreadId 獲得當前線程Id
驗證:在耗時非同步(寫入大字元串)操作前分別列印線程Id
Console.WriteLine(Thread.CurrentThread.ManagedThreadId); //托管線程id
StringBuilder sb = new StringBuilder(); //大文件
for (int i = 0; i < 2000; i++)
{
sb.Append("xxxxxxxxxxxxx"); //等待的過程中把當前的線程放到線程池,然後從線程池取出隨機線程執行別的方法
}
await File.WriteAllTextAsync(@"D:\text\1.txt",sb.ToString());
Console.WriteLine(Thread.CurrentThread.ManagedThreadId); //托管線程id 兩次線程的id不一樣
非同步編程不等於線程
非同步方法的代碼並不會自動在新線程中執行,除非把代碼放到新的線程中執行
用Task.Run改造之前的例子,再看看線程變化
Console.WriteLine("之前", +Thread.CurrentThread.ManagedThreadId);
double r = await CalcAsync(5000);
Console.WriteLine($"r={r}");
Console.WriteLine("之前", +Thread.CurrentThread.ManagedThreadId);
public static async Task<double> CalcAsync(int n)
{//自動根據返回值進行類型推斷 將下麵代碼放到新的線程執行
return await Task.Run(() =>
{
Console.WriteLine("CalcAsync", +Thread.CurrentThread.ManagedThreadId);
double result = 0;
Random radn = new Random();
for (int i = 0; i < n * n; i++)
{
result += radn.NextDouble();
}
return result;
});
}
anync方法缺點
1.非同步方法會生成一個類,運行效率沒有普通方法高,
2.可能會占用非常多的線程
只甩手Task,不拆完了再裝,反編譯上面的代碼,知識普通方法的調用
有點:運行效率高,不會造成線程浪費
返回值為Task的不一定都要標註async,標註async知識讓我們可以更方便的await而已
如果一個非同步的方法只是對別的方法調用的轉發,並沒有太多的複雜邏輯,(比如等待a的結果,再調用b,把a調用的返回值拿到內部做一些處理再返回),那麼就可以去掉async關鍵字
//內部只是對方法的調用
static Task<string> ReadAsync(int num)
{
if (num == 1)
{
return File.ReadAllTextAsync(@"D:\text\1.txt");
}
else if (num == 2)
{
return File.ReadAllTextAsync(@"D:\text\2.txt");
}
else
{
throw new ArgumentException();
}
}
非同步編程 不要用sleep
如果想在非同步方法中暫停一段時間,不要用thread.sleep(),因為他會阻塞調用線程,而要用await task.Delay()
在控制臺中沒看到區別,但是放到winform程式中就能看到區別了,asp.net core中也看不到區別但是sleep()會降低併發
//這是winform的代碼塊
private async void button1_Click(object sender, EventArgs e)
{
using (HttpClient httpClient = new HttpClient())
{
string sl = await
httpClient.GetStringAsync("https://www.youzack.com");
textBox1.Text = sl.Substring(0,20);
//Thread.Sleep(3000);
await Task.Delay(3000);
string s2 = await
httpClient.GetStringAsync("https://www.baidu.com");
textBox1.Text = sl.Substring(0, 20);
}
}
非同步編程CancellationToken
有時需要提前終止任務,比如:請求超時,用戶取消請求,跟多非同步方法都有CancellationToken參數 用於獲得提前終止的信號
CancellationToken結構體None:空 bool lsCancellationRequested是否取消(*)Register(Action callback)註冊取消監聽ThrowlfCancellationRequested()如果任務被取消,執行到這句話就拋異常。
CancellationTokenSource CancelAfter()超時後發出取消信號Cancel()發出取消信號 CancellationToken Token
為“下載一個網址N次”的方法增加取消功能。 分別用GetStringAsync + IsCancellationRequested、 GetStringAsync+ ThrowlfCancellationRequested()、帶CancellationToken的GetAsync()分別實現。取消分別用超時、用戶敲按鍵(不能await)實現。
ASP.NET Core開發中,一般不需要自己處理CancellationToken、 CancellationTokenSource這些,只要做到“能轉發cancellationToken就轉發”即可。ASP.NET Core會對於用戶請求中斷進行處理。(*)演示一下ASP.NETCore中的使用:寫一個方法,Delay1000次,用 Debug.WriteLine(輸出,訪問中間跳到放到其他網站。
CancellationTokenSource cts = new CancellationTokenSource();
cts.CancelAfter(2000);
CancellationToken token = cts.Token;
await DownloadlAsync("https://www.baidu.com",1000,token);
static async Task DownloadlAsync(string url, int n,CancellationToken cancellationToken)
{
using (HttpClient client = new HttpClient())
{
for (int i = 0; i < n; i++)
{
var resp = await client.GetAsync(url,cancellationToken);
string html = await resp.Content.ReadAsStringAsync();
File.WriteAllTextAsync(@"D:\text\1.txt", html,cancellationToken);
Console.WriteLine($"{DateTime.Now}:{html}");
//if (cancellationToken.IsCancellationRequested)
//{
// Console.WriteLine("請求被取消了");
// break;
//}
//cancellationToken.ThrowIfCancellationRequested();
}
}
}
//CancellationToken cancellationToken 這個參數是為了防止用戶訪問別的網站或者關閉瀏覽器服務端還在執行
static async Task DownloadlAsync(string url, int n, CancellationToken cancellationToken)
{
using (HttpClient client = new HttpClient())
{
for (int i = 0; i < n; i++)
{
var resp = await client.GetAsync(url, cancellationToken);
string html = await resp.Content.ReadAsStringAsync();
Debug.WriteLine(html);
}
}
}
public async Task<IActionResult> Index(CancellationToken cancellationToken)
{
await DownloadlAsync("https://www.baidu.com", 10000, cancellationToken);
return View();
}
非同步編程WhenAll
Task類的重要方法: 1.Task<Task> WhenAny(lEnumerable<Task>tasks)等,任何一個Task完成,Task就完成2.Task<TResult[]> WhenAll<TResult>(paramsTask<TResult>[] tasks)等,所有Task完成,Task才完成。用於等待多個任務執行結束,但是不在乎它們的執行順序。 3.FromResult()創建普通數值的Task對象。
string[] file = Directory.GetFiles(@"D:\text\1.txt");
Task<int>[] counts = new Task<int>[file.Length];
for (int i = 0; i < file.Length; i++)
{
string fileName = file[i];
Task<int> t = ReadCharCount(fileName);
counts[i] = t;
}
int[] countTask = await Task.WhenAll(counts);
Console.WriteLine(countTask);
static async Task<int> ReadCharCount(string fileName)
{
string a = await File.ReadAllTextAsync(fileName);
return a.Length;
}
非同步編程其他問題
介面中的非同步方法: async是提示編譯器為非同步方法中的await代碼進行分段處理的,而一個非同步方法是否修飾了async對於方法的調用者來講沒區別的,因此對於介面中的方法或者抽象方法不能修飾為async。
interface IText
{
Task<int> GetCharCount(string file);
}
class Text : IText
{
public async Task<int> GetCharCount(string file)
{
string a = await File.ReadAllTextAsync(file);
return a.Length;
}
}
非同步與yield: 複習: yield return不僅能夠簡化數據的返回,而且可以讓數據處理“流水線化”,提升性能。
foreach (var s in Test2())
{
Console.WriteLine(s);
}
static IEnumerable<string> Test2()
{
yield return "hellow"; //這裡直接返回一條進行foreach迴圈 取一條執行一條 方法內部拆分搞成狀態機
yield return "yzk";
yield return "youzack";
}
在舊版C#中,async方法中不能用yield。從C#8.0開始,把返回值聲明為IAsyncEnumerable(不要帶Task),然後遍歷的時候用await foreach()即可。
await foreach (var s in Test3())
{
Console.WriteLine(s);
}
static async IAsyncEnumerable<string> Test3()
{
yield return "hellow";
yield return "yzk";
yield return "youzack";
}
為什麼要學LING
為什麼要學LINQ?讓數據處理變得簡單: 讓數據處理傻瓜化 統計一個字元串中每個字母出現的頻率(忽略大小寫),然後按照從高到低的順序輸出出現頻率高於2次的單詞和其出現的頻率。
(複習)委托 1、委托是可以指向方法的類型,調用委托變數時執行的就是變數指向的方法。舉例。 2、.NET中定義了泛型委托Action(無返回值)和Func(有返回值),所以一般不用自定義委托類型。舉例。
static void Main(string[] args)
{
D1 d = F1;
d();
d = F2;
d();
D2 d2 = Add;
Console.WriteLine(d2(1,2));
Action a1 = F2;
a1();
Func<int, int, int> f = Add;
f(1,2);
Func<int, int, string> a = F33; //有返回值委托
a(1,2);
Action<int, string> c = F44; //無返回值委托
c(1,"zha");
}
static void F1()
{
Console.WriteLine("我是F1");
}
static void F2()
{
Console.WriteLine("我是F2");
}
static int Add(int a,int b)
{
return a + b;
}
static string F33(int i,int a)
{
return "hellow";
}
static void F44(int a,string b)
{
}
}
delegate void D1();
delegate int D2(int a,int b);
Lambda表達式
可以省略參數數據類型,因為編譯能根據委托類型推斷出參數的類型,用=>引出來方法體。
一步一步簡化
Action f1 = delegate ()
{
Console.WriteLine("我是方法");
};
Action f11 = () => Console.WriteLine("我是方法");
f1 (); //匿名方法無參無返回值
Action<string, int> f2 = (string a, int b) => Console.WriteLine($"a={a},b={b}"); ;
f2("zyb",1); //匿名方法 兩個參數
Func<int, int, int> f3 = (int a, int b) => { return a + b; };
f3(1,2); //有參有返回值 匿名方法
Func<int, int, int> f4 = (int i, int j) => i + j;
f4(1,2);
Func<int, int, int> f5 = ( i, j) =>
{
return i + j;
};
f5(1, 2);
Action<int> a1 = i => Console.WriteLine(i);
Func<int, bool> f66 = delegate (int i)
{
return i > 0;
};
Func<int, bool> f67 = i => i > 0;
Func<int, bool> f68 = delegate (int i)
{
return i > 5;
};
}
LING方法的背後
LING中提供了很多集合的擴展方法 配合lambda能簡化數據處理
static void Main(string[] args)
{
int[] nums = new int[] { 20,55,51,55,58,99,22};
//where方法會遍歷集合中每個元素 對於每個元素
//都調用a=>a>10 這個表達式判斷一下是否為true
//如果為true 則把這個放到返回的集合中
//IEnumerable<int> result = nums.Where(a=>a>10);
var result = MyWhere2(nums,a => a > 10);
foreach (int item in result)
{
Console.WriteLine(item);
}
}
static IEnumerable<int> MyWhere(IEnumerable<int> items,Func<int,bool> f)
{
List<int> result = new List<int>();
foreach (int item in result)
{
if (f(item)==true)
{
result.Add(item);
}
}
return result;
}
static IEnumerable<int> MyWhere2(IEnumerable<int> items, Func<int, bool> f)
{
foreach (int item in items)
{
if (f(item) == true)
{
yield return item; //流水線處理 滿足條件直接返回
}
}
}
}
LING常用擴展方法 IEnumberble<T> 擴展方法
獲取一條數據(是否帶參數的兩種寫法):Single:有且只有一條滿足要求的數據;SingleOrDefault:最多只有一條滿足要求的數據; First :至少有一條,返回第一條; FirstOrDefault:返回第一條或者預設值; 選擇合適的方法,“防禦性編程”
LING解決面試
性能與面試 LINQ大部分時間不會影響性能,不過我曾經遇到過,講講。 面試時候的演算法題一般儘量避免使用正則表達式、LINQ等這些高級的類庫。大飯店面試大廚的故事。
案例1 有一個用逗號分隔的表示成績的字元串,如 "61,90,100,99,18,22,38,66,80,93,55,50,89",計算這些成績的平均值。
string s = "33,55,66,99";
double avg2 = s.Split(',').Select(e => Convert.ToInt32(e)).Average();
Console.WriteLine(avg);
案例2 統計一個字元串中每個字母出現的頻率(忽略大小寫),然後按照從高到低的順序輸出出現頻率高於2次的單詞和其出現的頻率。
string s = "fjdssl fjsdj ejejejej dfaSFSDd";
var item = s.Where(c => char.IsLetter(c)).Select(c => char.ToLower(c)) //過濾掉空格逗號
.GroupBy(c => c).Select(g => new { g.Key, Count = g.Count() })
.OrderByDescending(g => g.Count).Where(g => g.Count > 2);
foreach (var c in item)
{
Console.WriteLine(c);
}
依賴註入 控制反轉
概念 生活中的“控制反轉”:自己發電和用電網的電。 依賴註入(Dependency Injection,Dl)是控制反轉(lnversion of Control,IOC)思想的實現方式。 依賴註入簡化模塊的組裝過程,降低模塊之間的耦合度
自己發電的代碼
var connSettings =
ConfigurationManager.ConnectionStrings["connStr1"];
string connStr = connSettings.ConnectionString;SqIConnection conn = new
SqlConnection(connStr);缺點是?
//自己從配置文件取到連接字元串 然後實例一個連接資料庫的連接把字元串放進去
代碼控制反轉的目的 “怎樣創建XX對象"→“我要XX對象” 兩種實現方式: 1)服務定位器(ServiceLocator); 2)依賴註入(Dependency Injection,Dl);
DI幾個概念 服務(service):對象;註冊服務; 服務容器:負責管理註冊的服務;查詢服務:創建對象及關聯對象; 對象生命周期:Transient(瞬態);Scoped(範圍) ; Singleton(單例);
.NET中使用DI 2、根據類型來獲取和註冊服務。 可以分別指定服務類型(service type) 和實現類 型(implementation type)。這兩者可能相同,也可能不同。服務類型可以是類,也可以是介面,建議面向介面編程,更靈活。 3、.NET控制反轉組件取名為Dependencylnjection,但它包含ServiceLocator的功能。
生命周期
生命周期 1、給類構造函數中列印,看看不同生命周期的對象創建,使用serviceProvider.CreateScope()創建Scope。 2、如果一個類實現了IDisposable介面,則離開作用域之後容器會自動調用對象的Dispose方法。 3、不要在長生命周期的對象中引用比它短的生命周期的對象。在ASP.NET Core中,這樣做預設會拋異常。 4、生命周期的選擇:如果類無狀態,建議為Singleton;如果類有狀態,且有Scope控制,建議為Scoped,因為通常這種Scope控制下的代碼都是運行在同一個線程中的,沒有併發修改的問題;在使用Transient的時候要謹慎。 5、.NET註冊服務的重載方法很多,看著文檔琢磨吧。
//使用ioc容器
ServiceCollection services = new ServiceCollection();
services.AddTransient<TestSrvicelmpl>(); // 調用一次實例新的對象 瞬態
//services.AddSingleton<TestSrvicelmpl>(); //相同的對象 單例
//services.AddScoped<TestSrvicelmpl>(); //在範圍內拿到的是同一個對象
using (ServiceProvider sp = services.BuildServiceProvider()) //ServiceProvider相當於服務定位器
{
//TestSrvicelmpl T = sp.GetService<TestSrvicelmpl>();
//T.Name = "ZHAGNSNA";
//T.SayHi();
//TestSrvicelmpl T1 = sp.GetService<TestSrvicelmpl>();
//T1.Name = "放假啊可大幅度";
//T1.SayHi();
////比較兩個是否同一個對象 結果false
//Console.WriteLine(object.ReferenceEquals(T, T1));
//T.SayHi();
using (IServiceScope scope1 = sp.CreateScope())
{
TestSrvicelmpl T = scope1.ServiceProvider.GetService<TestSrvicelmpl>();
T.Name = "ZHAGNSNA";
T.SayHi();
TestSrvicelmpl T1 = scope1.ServiceProvider.GetService<TestSrvicelmpl>();
Console.WriteLine(object.ReferenceEquals(T, T1));
}
using (IServiceScope scope2 = sp.CreateScope())
{
TestSrvicelmpl T = scope2.ServiceProvider.GetService<TestSrvicelmpl>();
T.Name = "ZHAGNSNA";
T.SayHi();
TestSrvicelmpl T1 = scope2.ServiceProvider.GetService<TestSrvicelmpl>();
Console.WriteLine(object.ReferenceEquals(T, T1));
}
}
IServiceProvider的服務定位器方法:
T GetService<T>()如果獲取不到對象,則返回null。object GetService(Type serviceType) T GetRequiredService<T>()如果獲取不到對象,則拋異常 object GetRequiredService(Type serviceType) lEnumerable<T> GetServices<T>()適用於可能有很多滿足條件的服務 lEnumerable<object> GetServices(Type serviceType)
ServiceCollection services = new ServiceCollection();
services.AddScoped<ITestServie, TestSrvicelmpl>();
services.AddScoped<ITestServie, TestSrvicelmpl2>();
services.AddSingleton(typeof(ITestServie),new TestSrvicelmpl());
using (ServiceProvider sp = services.BuildServiceProvider())
{
//GetService找不到服務返回null
//ITestServie ts1 = sp.GetService<ITestServie>();
//ts1.Name = "tom";
//ts1.SayHi();
//Console.WriteLine(ts1.GetType());
//GetRequiredService 必須的如果找不到 直接拋異常
//ITestServie ts1 = sp.GetRequiredService<ITestServie>();
//ts1.Name = "tom";
//ts1.SayHi();
//Console.WriteLine(ts1.GetType());
//註冊多個服務
//IEnumerable<ITestServie> tets = sp.GetServices<ITestServie>();
// foreach (ITestServie item in tets)
// {
// Console.WriteLine(item.GetType());
// }
ITestServie test1 = sp.GetService<ITestServie>();
Console.WriteLine(test1.GetType());
}
DI魅力漸顯:依賴註入
1、依賴註入是有傳染性”的,如果一個類的對象是通過DI創建的,那麼這個奚的構造函數中聲明的所有服務類型的參數都會被DI賦值;但是如果一個對象是程式員手動創建的,那麼這個對象就和DI沒有關係,它的構造函數中聲明的服務類型參數就不會被自動賦值。 2、.NET的DI預設是構造函數註入。 3、舉例:編寫一個類,連接資料庫做插入操作,並且記錄日誌(模擬的輸出),把Dao、日誌都放入單獨的服務類。connstr見備註。
static void Main(string[] args)
{
//降低模塊之間的耦合
ServiceCollection services = new ServiceCollection();
services.AddScoped<Controller>();
services.AddScoped<ILog,LogImp1>();
services.AddScoped<IStorage,StorageImp1>();
services.AddScoped<IConfig,ConfiImp1>();
using (var sp = services.BuildServiceProvider())
{
var c = sp.GetService<Controller>();
c.Test();
}
Console.ReadKey();
}
class Controller
{
private readonly ILog log;
private readonly IStorage storage;
public Controller(ILog log, IStorage storage)
{
this.log = log;
this.storage = storage;
}
public void Test()
{
log.Log("開始上傳");
storage.Save("fddfs","1.txt");
log.Log("上傳完畢");
}
}
interface ILog
{
public void Log(string msg);
}
public class LogImp1 : ILog
{
public void Log(string msg)
{
Console.WriteLine($"日誌:{msg}");
}
}
interface IConfig
{
public string GetValue(string name);
}
class ConfiImp1 : IConfig
{
public string GetValue(string name)
{
return "config";
}
}
interface IStorage
{
public void Save(string content,string name);
}
class StorageImp1 : IStorage
{
private readonly IConfig config;
public StorageImp1(IConfig config)
{
this.config = config;
}
public void Save(string content, string name)
{
string server = config.GetValue("server");
Console.WriteLine($"向伺服器{server}的文件名為{name}上傳{content}");
}
}